/* * Copyright (C) 2007-2008 Esmertec AG. * Copyright (C) 2007-2008 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.mms.dom.smil; import org.w3c.dom.NodeList; import org.w3c.dom.events.DocumentEvent; import org.w3c.dom.events.Event; import org.w3c.dom.events.EventTarget; import org.w3c.dom.smil.ElementParallelTimeContainer; import org.w3c.dom.smil.ElementSequentialTimeContainer; import org.w3c.dom.smil.ElementTime; import org.w3c.dom.smil.Time; import org.w3c.dom.smil.TimeList; import android.util.Log; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashSet; /** * The SmilPlayer is responsible for playing, stopping, pausing and resuming a SMIL tree. * <li>It creates a whole timeline before playing.</li> * <li>The player runs in a different thread which intends not to block the main thread.</li> */ public class SmilPlayer implements Runnable { private static final String TAG = "Mms/smil"; private static final boolean DEBUG = false; private static final boolean LOCAL_LOGV = false; private static final int TIMESLICE = 200; private static enum SmilPlayerState { INITIALIZED, PLAYING, PLAYED, PAUSED, STOPPED, } private static enum SmilPlayerAction { NO_ACTIVE_ACTION, RELOAD, STOP, PAUSE, START, NEXT, PREV } public static final String MEDIA_TIME_UPDATED_EVENT = "mediaTimeUpdated"; private static final Comparator<TimelineEntry> sTimelineEntryComparator = new Comparator<TimelineEntry>() { public int compare(TimelineEntry o1, TimelineEntry o2) { return Double.compare(o1.getOffsetTime(), o2.getOffsetTime()); } }; private static SmilPlayer sPlayer; private long mCurrentTime; private int mCurrentElement; private int mCurrentSlide; private ArrayList<TimelineEntry> mAllEntries; private ElementTime mRoot; private Thread mPlayerThread; private SmilPlayerState mState = SmilPlayerState.INITIALIZED; private SmilPlayerAction mAction = SmilPlayerAction.NO_ACTIVE_ACTION; private ArrayList<ElementTime> mActiveElements; private Event mMediaTimeUpdatedEvent; private static ArrayList<TimelineEntry> getParTimeline( ElementParallelTimeContainer par, double offset, double maxOffset) { ArrayList<TimelineEntry> timeline = new ArrayList<TimelineEntry>(); // Set my begin at first TimeList myBeginList = par.getBegin(); /* * Begin list only contain 1 begin time which has been resolved. * @see com.android.mms.dom.smil.ElementParallelTimeContainerImpl#getBegin() */ Time begin = myBeginList.item(0); double beginOffset = begin.getResolvedOffset() + offset; if (beginOffset > maxOffset) { // This element can't be started. return timeline; } TimelineEntry myBegin = new TimelineEntry(beginOffset, par, TimelineEntry.ACTION_BEGIN); timeline.add(myBegin); TimeList myEndList = par.getEnd(); /* * End list only contain 1 end time which has been resolved. * @see com.android.mms.dom.smil.ElementParallelTimeContainerImpl#getEnd() */ Time end = myEndList.item(0); double endOffset = end.getResolvedOffset() + offset; if (endOffset > maxOffset) { endOffset = maxOffset; } TimelineEntry myEnd = new TimelineEntry(endOffset, par, TimelineEntry.ACTION_END); maxOffset = endOffset; NodeList children = par.getTimeChildren(); for (int i = 0; i < children.getLength(); ++i) { ElementTime child = (ElementTime) children.item(i); ArrayList<TimelineEntry> childTimeline = getTimeline(child, offset, maxOffset); timeline.addAll(childTimeline); } Collections.sort(timeline, sTimelineEntryComparator); // Add end-event to timeline for all active children NodeList activeChildrenAtEnd = par.getActiveChildrenAt( (float) (endOffset - offset) * 1000); for (int i = 0; i < activeChildrenAtEnd.getLength(); ++i) { timeline.add(new TimelineEntry(endOffset, (ElementTime) activeChildrenAtEnd.item(i), TimelineEntry.ACTION_END)); } // Set my end at last timeline.add(myEnd); return timeline; } private static ArrayList<TimelineEntry> getSeqTimeline( ElementSequentialTimeContainer seq, double offset, double maxOffset) { ArrayList<TimelineEntry> timeline = new ArrayList<TimelineEntry>(); double orgOffset = offset; // Set my begin at first TimeList myBeginList = seq.getBegin(); /* * Begin list only contain 1 begin time which has been resolved. * @see com.android.mms.dom.smil.ElementSequentialTimeContainerImpl#getBegin() */ Time begin = myBeginList.item(0); double beginOffset = begin.getResolvedOffset() + offset; if (beginOffset > maxOffset) { // This element can't be started. return timeline; } TimelineEntry myBegin = new TimelineEntry(beginOffset, seq, TimelineEntry.ACTION_BEGIN); timeline.add(myBegin); TimeList myEndList = seq.getEnd(); /* * End list only contain 1 end time which has been resolved. * @see com.android.mms.dom.smil.ElementSequentialTimeContainerImpl#getEnd() */ Time end = myEndList.item(0); double endOffset = end.getResolvedOffset() + offset; if (endOffset > maxOffset) { endOffset = maxOffset; } TimelineEntry myEnd = new TimelineEntry(endOffset, seq, TimelineEntry.ACTION_END); maxOffset = endOffset; // Get children's timelines NodeList children = seq.getTimeChildren(); for (int i = 0; i < children.getLength(); ++i) { ElementTime child = (ElementTime) children.item(i); ArrayList<TimelineEntry> childTimeline = getTimeline(child, offset, maxOffset); timeline.addAll(childTimeline); // Since the child timeline has been sorted, the offset of the last one is the biggest. offset = childTimeline.get(childTimeline.size() - 1).getOffsetTime(); } // Add end-event to timeline for all active children NodeList activeChildrenAtEnd = seq.getActiveChildrenAt( (float) (endOffset - orgOffset)); for (int i = 0; i < activeChildrenAtEnd.getLength(); ++i) { timeline.add(new TimelineEntry(endOffset, (ElementTime) activeChildrenAtEnd.item(i), TimelineEntry.ACTION_END)); } // Set my end at last timeline.add(myEnd); return timeline; } private static ArrayList<TimelineEntry> getTimeline(ElementTime element, double offset, double maxOffset) { if (element instanceof ElementParallelTimeContainer) { return getParTimeline((ElementParallelTimeContainer) element, offset, maxOffset); } else if (element instanceof ElementSequentialTimeContainer) { return getSeqTimeline((ElementSequentialTimeContainer) element, offset, maxOffset); } else { // Not ElementTimeContainer here ArrayList<TimelineEntry> timeline = new ArrayList<TimelineEntry>(); TimeList beginList = element.getBegin(); for (int i = 0; i < beginList.getLength(); ++i) { Time begin = beginList.item(i); if (begin.getResolved()) { double beginOffset = begin.getResolvedOffset() + offset; if (beginOffset <= maxOffset) { TimelineEntry entry = new TimelineEntry(beginOffset, element, TimelineEntry.ACTION_BEGIN); timeline.add(entry); } } } TimeList endList = element.getEnd(); for (int i = 0; i < endList.getLength(); ++i) { Time end = endList.item(i); if (end.getResolved()) { double endOffset = end.getResolvedOffset() + offset; if (endOffset <= maxOffset) { TimelineEntry entry = new TimelineEntry(endOffset, element, TimelineEntry.ACTION_END); timeline.add(entry); } } } Collections.sort(timeline, sTimelineEntryComparator); return timeline; } } private SmilPlayer() { // Private constructor } public static SmilPlayer getPlayer() { if (sPlayer == null) { sPlayer = new SmilPlayer(); } return sPlayer; } public synchronized boolean isPlayingState() { return mState == SmilPlayerState.PLAYING; } public synchronized boolean isPlayedState() { return mState == SmilPlayerState.PLAYED; } public synchronized boolean isPausedState() { return mState == SmilPlayerState.PAUSED; } public synchronized boolean isStoppedState() { return mState == SmilPlayerState.STOPPED; } private synchronized boolean isPauseAction() { return mAction == SmilPlayerAction.PAUSE; } private synchronized boolean isStartAction() { return mAction == SmilPlayerAction.START; } private synchronized boolean isStopAction() { return mAction == SmilPlayerAction.STOP; } private synchronized boolean isReloadAction() { return mAction == SmilPlayerAction.RELOAD; } private synchronized boolean isNextAction() { return mAction == SmilPlayerAction.NEXT; } private synchronized boolean isPrevAction() { return mAction == SmilPlayerAction.PREV; } public synchronized void init(ElementTime root) { mRoot = root; mAllEntries = getTimeline(mRoot, 0, Long.MAX_VALUE); mMediaTimeUpdatedEvent = ((DocumentEvent) mRoot).createEvent("Event"); mMediaTimeUpdatedEvent.initEvent(MEDIA_TIME_UPDATED_EVENT, false, false); mActiveElements = new ArrayList<ElementTime>(); } public synchronized void play() { if (!isPlayingState()) { mCurrentTime = 0; mCurrentElement = 0; mCurrentSlide = 0; mPlayerThread = new Thread(this); mState = SmilPlayerState.PLAYING; mPlayerThread.start(); } else { Log.w(TAG, "Error State: Playback is playing!"); } } public synchronized void pause() { if (isPlayingState()) { mAction = SmilPlayerAction.PAUSE; notifyAll(); } else { Log.w(TAG, "Error State: Playback is not playing!"); } } public synchronized void start() { if (isPausedState()) { resumeActiveElements(); mAction = SmilPlayerAction.START; notifyAll(); } else if (isPlayedState()) { play(); } else { Log.w(TAG, "Error State: Playback can not be started!"); } } public synchronized void stop() { if (isPlayingState() || isPausedState()) { mAction = SmilPlayerAction.STOP; notifyAll(); } else if (isPlayedState()) { actionStop(); } } public synchronized void stopWhenReload() { endActiveElements(); } public synchronized void reload() { if (isPlayingState() || isPausedState()) { mAction = SmilPlayerAction.RELOAD; notifyAll(); } else if (isPlayedState()) { actionReload(); } } public synchronized void next() { if (isPlayingState() || isPausedState()) { mAction = SmilPlayerAction.NEXT; notifyAll(); } } public synchronized void prev() { if (isPlayingState() || isPausedState()) { mAction = SmilPlayerAction.PREV; notifyAll(); } } private synchronized boolean isBeginOfSlide(TimelineEntry entry) { return (TimelineEntry.ACTION_BEGIN == entry.getAction()) && (entry.getElement() instanceof SmilParElementImpl); } private synchronized void reloadActiveSlide() { mActiveElements.clear(); beginSmilDocument(); for (int i = mCurrentSlide; i < mCurrentElement; i++) { TimelineEntry entry = mAllEntries.get(i); actionEntry(entry); } seekActiveMedia(); } private synchronized void beginSmilDocument() { TimelineEntry entry = mAllEntries.get(0); actionEntry(entry); } private synchronized double getOffsetTime(ElementTime element) { for (int i = mCurrentSlide; i < mCurrentElement; i++) { TimelineEntry entry = mAllEntries.get(i); if (element.equals(entry.getElement())) { return entry.getOffsetTime() * 1000; // in ms } } return -1; } private synchronized void seekActiveMedia() { for (int i = mActiveElements.size() - 1; i >= 0; i--) { ElementTime element = mActiveElements.get(i); if (element instanceof SmilParElementImpl) { return; } double offset = getOffsetTime(element); if ((offset >= 0) && (offset <= mCurrentTime)) { if (LOCAL_LOGV) { Log.v(TAG, "[SEEK] " + " at " + mCurrentTime + " " + element); } element.seekElement( (float) (mCurrentTime - offset) ); } } } private synchronized void waitForEntry(long interval) throws InterruptedException { if (LOCAL_LOGV) { Log.v(TAG, "Waiting for " + interval + "ms."); } long overhead = 0; while (interval > 0) { long startAt = System.currentTimeMillis(); long sleep = Math.min(interval, TIMESLICE); if (overhead < sleep) { wait(sleep - overhead); mCurrentTime += sleep; } else { sleep = 0; mCurrentTime += overhead; } if (isStopAction() || isReloadAction() || isPauseAction() || isNextAction() || isPrevAction()) { return; } ((EventTarget) mRoot).dispatchEvent(mMediaTimeUpdatedEvent); interval -= TIMESLICE; overhead = System.currentTimeMillis() - startAt - sleep; } } public synchronized int getDuration() { if ((mAllEntries != null) && !mAllEntries.isEmpty()) { return (int) mAllEntries.get(mAllEntries.size() - 1).mOffsetTime * 1000; } return 0; } public synchronized int getCurrentPosition() { return (int) mCurrentTime; } private synchronized void endActiveElements() { for (int i = mActiveElements.size() - 1; i >= 0; i--) { ElementTime element = mActiveElements.get(i); if (LOCAL_LOGV) { Log.v(TAG, "[STOP] " + " at " + mCurrentTime + " " + element); } element.endElement(); } } private synchronized void pauseActiveElements() { for (int i = mActiveElements.size() - 1; i >= 0; i--) { ElementTime element = mActiveElements.get(i); if (LOCAL_LOGV) { Log.v(TAG, "[PAUSE] " + " at " + mCurrentTime + " " + element); } element.pauseElement(); } } private synchronized void resumeActiveElements() { int size = mActiveElements.size(); for (int i = 0; i < size; i++) { ElementTime element = mActiveElements.get(i); if (LOCAL_LOGV) { Log.v(TAG, "[RESUME] " + " at " + mCurrentTime + " " + element); } element.resumeElement(); } } private synchronized void waitForWakeUp() { try { while ( !(isStartAction() || isStopAction() || isReloadAction() || isNextAction() || isPrevAction()) ) { wait(TIMESLICE); } if (isStartAction()) { mAction = SmilPlayerAction.NO_ACTIVE_ACTION; mState = SmilPlayerState.PLAYING; } } catch (InterruptedException e) { Log.e(TAG, "Unexpected InterruptedException.", e); } } private synchronized void actionEntry(TimelineEntry entry) { switch (entry.getAction()) { case TimelineEntry.ACTION_BEGIN: if (LOCAL_LOGV) { Log.v(TAG, "[START] " + " at " + mCurrentTime + " " + entry.getElement()); } entry.getElement().beginElement(); mActiveElements.add(entry.getElement()); break; case TimelineEntry.ACTION_END: if (LOCAL_LOGV) { Log.v(TAG, "[STOP] " + " at " + mCurrentTime + " " + entry.getElement()); } entry.getElement().endElement(); mActiveElements.remove(entry.getElement()); break; default: break; } } private synchronized TimelineEntry reloadCurrentEntry() { // Check if the position is less than size of all entries if (mCurrentElement < mAllEntries.size()) { return mAllEntries.get(mCurrentElement); } else { return null; } } private void stopCurrentSlide() { HashSet<TimelineEntry> skippedEntries = new HashSet<TimelineEntry>(); int totalEntries = mAllEntries.size(); for (int i = mCurrentElement; i < totalEntries; i++) { // Stop any started entries, and skip the not started entries until // meeting the end of slide TimelineEntry entry = mAllEntries.get(i); int action = entry.getAction(); if (entry.getElement() instanceof SmilParElementImpl && action == TimelineEntry.ACTION_END) { actionEntry(entry); mCurrentElement = i; break; } else if (action == TimelineEntry.ACTION_END && !skippedEntries.contains(entry)) { actionEntry(entry); } else if (action == TimelineEntry.ACTION_BEGIN) { skippedEntries.add(entry); } } } private TimelineEntry loadNextSlide() { TimelineEntry entry; int totalEntries = mAllEntries.size(); for (int i = mCurrentElement; i < totalEntries; i++) { entry = mAllEntries.get(i); if (isBeginOfSlide(entry)) { mCurrentElement = i; mCurrentSlide = i; mCurrentTime = (long)(entry.getOffsetTime() * 1000); return entry; } } // No slide, finish play back mCurrentElement++; entry = null; if (mCurrentElement < totalEntries) { entry = mAllEntries.get(mCurrentElement); mCurrentTime = (long)(entry.getOffsetTime() * 1000); } return entry; } private TimelineEntry loadPrevSlide() { int skippedSlides = 1; int latestBeginEntryIndex = -1; for (int i = mCurrentSlide; i >= 0; i--) { TimelineEntry entry = mAllEntries.get(i); if (isBeginOfSlide(entry)) { latestBeginEntryIndex = i; if (0 == skippedSlides-- ) { mCurrentElement = i; mCurrentSlide = i; mCurrentTime = (long)(entry.getOffsetTime() * 1000); return entry; } } } if (latestBeginEntryIndex != -1) { mCurrentElement = latestBeginEntryIndex; mCurrentSlide = latestBeginEntryIndex; return mAllEntries.get(mCurrentElement); } return null; } private synchronized TimelineEntry actionNext() { stopCurrentSlide(); return loadNextSlide(); } private synchronized TimelineEntry actionPrev() { stopCurrentSlide(); return loadPrevSlide(); } private synchronized void actionPause() { pauseActiveElements(); mState = SmilPlayerState.PAUSED; mAction = SmilPlayerAction.NO_ACTIVE_ACTION; } private synchronized void actionStop() { endActiveElements(); mCurrentTime = 0; mCurrentElement = 0; mCurrentSlide = 0; mState = SmilPlayerState.STOPPED; mAction = SmilPlayerAction.NO_ACTIVE_ACTION; } private synchronized void actionReload() { reloadActiveSlide(); mAction = SmilPlayerAction.NO_ACTIVE_ACTION; } public void run() { if (isStoppedState()) { return; } if (LOCAL_LOGV) { dumpAllEntries(); } // Play the Element by following the timeline int size = mAllEntries.size(); for (mCurrentElement = 0; mCurrentElement < size; mCurrentElement++) { TimelineEntry entry = mAllEntries.get(mCurrentElement); if (isBeginOfSlide(entry)) { mCurrentSlide = mCurrentElement; } long offset = (long) (entry.getOffsetTime() * 1000); // in ms. while (offset > mCurrentTime) { try { waitForEntry(offset - mCurrentTime); } catch (InterruptedException e) { Log.e(TAG, "Unexpected InterruptedException.", e); } while (isPauseAction() || isStopAction() || isReloadAction() || isNextAction() || isPrevAction()) { if (isPauseAction()) { actionPause(); waitForWakeUp(); } if (isStopAction()) { actionStop(); return; } if (isReloadAction()) { actionReload(); entry = reloadCurrentEntry(); if (entry == null) return; if (isPausedState()) { mAction = SmilPlayerAction.PAUSE; } } if (isNextAction()) { TimelineEntry nextEntry = actionNext(); if (nextEntry != null) { entry = nextEntry; } if (mState == SmilPlayerState.PAUSED) { mAction = SmilPlayerAction.PAUSE; actionEntry(entry); } else { mAction = SmilPlayerAction.NO_ACTIVE_ACTION; } offset = mCurrentTime; } if (isPrevAction()) { TimelineEntry prevEntry = actionPrev(); if (prevEntry != null) { entry = prevEntry; } if (mState == SmilPlayerState.PAUSED) { mAction = SmilPlayerAction.PAUSE; actionEntry(entry); } else { mAction = SmilPlayerAction.NO_ACTIVE_ACTION; } offset = mCurrentTime; } } } mCurrentTime = offset; actionEntry(entry); } mState = SmilPlayerState.PLAYED; } private static final class TimelineEntry { final static int ACTION_BEGIN = 0; final static int ACTION_END = 1; private final double mOffsetTime; private final ElementTime mElement; private final int mAction; public TimelineEntry(double offsetTime, ElementTime element, int action) { mOffsetTime = offsetTime; mElement = element; mAction = action; } public double getOffsetTime() { return mOffsetTime; } public ElementTime getElement() { return mElement; } public int getAction() { return mAction; } public String toString() { return "Type = " + mElement + " offset = " + getOffsetTime() + " action = " + getAction(); } } private void dumpAllEntries() { if (LOCAL_LOGV) { for (TimelineEntry entry : mAllEntries) { Log.v(TAG, "[Entry] "+ entry); } } } }