FileDocCategorySizeDatePackage
SmilPlayer.javaAPI DocAndroid 1.5 API20708Wed May 06 22:42:46 BST 2009com.android.mms.dom.smil

SmilPlayer.java

/*
 * 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.Config;
import android.util.Log;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;

/**
 * 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 = "SmilPlayer";
    private static final boolean DEBUG = false;
    private static final boolean LOCAL_LOGV = DEBUG ? Config.LOGD : Config.LOGV;
    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,
    }

    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;
    }

    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();
        }
    }

    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()) {
                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()) ) {
                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() {
        return mAllEntries.get(mCurrentElement);
    }

    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;
        }

        // 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()) {
                    if (isPauseAction()) {
                        actionPause();
                        waitForWakeUp();
                    }

                    if (isStopAction()) {
                        actionStop();
                        return;
                    }

                    if (isReloadAction()) {
                        actionReload();
                        entry = reloadCurrentEntry();
                        if (isPausedState()) {
                            mAction = SmilPlayerAction.PAUSE;
                        }
                    }
                }
            }
            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;
        }
    }
}