/*
* 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 com.android.mms.LogTag;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
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;
/**
* The SmilPlayer is responsible for playing, stopping, pausing and resuming a SMIL tree.
*
It creates a whole timeline before playing.
* The player runs in a different thread which intends not to block the main thread.
*/
public class SmilPlayer implements Runnable {
private static final String TAG = LogTag.TAG;
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 sTimelineEntryComparator =
new Comparator() {
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 mAllEntries;
private ElementTime mRoot;
private Thread mPlayerThread;
private SmilPlayerState mState = SmilPlayerState.INITIALIZED;
private SmilPlayerAction mAction = SmilPlayerAction.NO_ACTIVE_ACTION;
private ArrayList mActiveElements;
private Event mMediaTimeUpdatedEvent;
private static ArrayList getParTimeline(
ElementParallelTimeContainer par, double offset, double maxOffset) {
ArrayList timeline = new ArrayList();
// 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 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 getSeqTimeline(
ElementSequentialTimeContainer seq, double offset, double maxOffset) {
ArrayList timeline = new ArrayList();
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 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 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 timeline = new ArrayList();
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();
}
public synchronized void play() {
if (!isPlayingState()) {
mCurrentTime = 0;
mCurrentElement = 0;
mCurrentSlide = 0;
mPlayerThread = new Thread(this, "SmilPlayer thread");
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 skippedEntries = new HashSet();
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);
}
}
}
}