1/*
2 * Copyright (C) 2007-2008 Esmertec AG.
3 * Copyright (C) 2007-2008 The Android Open Source Project
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *      http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18package com.android.mms.dom.smil;
19
20import java.util.ArrayList;
21import java.util.Collections;
22import java.util.Comparator;
23import java.util.HashSet;
24
25import org.w3c.dom.NodeList;
26import org.w3c.dom.events.DocumentEvent;
27import org.w3c.dom.events.Event;
28import org.w3c.dom.events.EventTarget;
29import org.w3c.dom.smil.ElementParallelTimeContainer;
30import org.w3c.dom.smil.ElementSequentialTimeContainer;
31import org.w3c.dom.smil.ElementTime;
32import org.w3c.dom.smil.Time;
33import org.w3c.dom.smil.TimeList;
34
35import android.util.Log;
36
37/**
38 * The SmilPlayer is responsible for playing, stopping, pausing and resuming a SMIL tree.
39 * <li>It creates a whole timeline before playing.</li>
40 * <li>The player runs in a different thread which intends not to block the main thread.</li>
41 */
42public class SmilPlayer implements Runnable {
43    private static final String TAG = "Mms/smil";
44    private static final boolean DEBUG = false;
45    private static final boolean LOCAL_LOGV = false;
46    private static final int TIMESLICE = 200;
47
48    private static enum SmilPlayerState {
49        INITIALIZED,
50        PLAYING,
51        PLAYED,
52        PAUSED,
53        STOPPED,
54    }
55
56    private static enum SmilPlayerAction {
57        NO_ACTIVE_ACTION,
58        RELOAD,
59        STOP,
60        PAUSE,
61        START,
62        NEXT,
63        PREV
64    }
65
66    public static final String MEDIA_TIME_UPDATED_EVENT = "mediaTimeUpdated";
67
68    private static final Comparator<TimelineEntry> sTimelineEntryComparator =
69        new Comparator<TimelineEntry>() {
70        public int compare(TimelineEntry o1, TimelineEntry o2) {
71            return Double.compare(o1.getOffsetTime(), o2.getOffsetTime());
72        }
73    };
74
75    private static SmilPlayer sPlayer;
76
77    private long mCurrentTime;
78    private int mCurrentElement;
79    private int mCurrentSlide;
80    private ArrayList<TimelineEntry> mAllEntries;
81    private ElementTime mRoot;
82    private Thread mPlayerThread;
83    private SmilPlayerState mState = SmilPlayerState.INITIALIZED;
84    private SmilPlayerAction mAction = SmilPlayerAction.NO_ACTIVE_ACTION;
85    private ArrayList<ElementTime> mActiveElements;
86    private Event mMediaTimeUpdatedEvent;
87
88    private static ArrayList<TimelineEntry> getParTimeline(
89            ElementParallelTimeContainer par, double offset, double maxOffset) {
90        ArrayList<TimelineEntry> timeline = new ArrayList<TimelineEntry>();
91
92        // Set my begin at first
93        TimeList myBeginList = par.getBegin();
94        /*
95         * Begin list only contain 1 begin time which has been resolved.
96         * @see com.android.mms.dom.smil.ElementParallelTimeContainerImpl#getBegin()
97         */
98        Time begin = myBeginList.item(0);
99        double beginOffset = begin.getResolvedOffset() + offset;
100        if (beginOffset > maxOffset) {
101            // This element can't be started.
102            return timeline;
103        }
104        TimelineEntry myBegin = new TimelineEntry(beginOffset, par, TimelineEntry.ACTION_BEGIN);
105        timeline.add(myBegin);
106
107        TimeList myEndList = par.getEnd();
108        /*
109         * End list only contain 1 end time which has been resolved.
110         * @see com.android.mms.dom.smil.ElementParallelTimeContainerImpl#getEnd()
111         */
112        Time end = myEndList.item(0);
113        double endOffset = end.getResolvedOffset() + offset;
114        if (endOffset > maxOffset) {
115            endOffset = maxOffset;
116        }
117        TimelineEntry myEnd = new TimelineEntry(endOffset, par, TimelineEntry.ACTION_END);
118
119        maxOffset = endOffset;
120
121        NodeList children = par.getTimeChildren();
122        for (int i = 0; i < children.getLength(); ++i) {
123            ElementTime child = (ElementTime) children.item(i);
124            ArrayList<TimelineEntry> childTimeline = getTimeline(child, offset, maxOffset);
125            timeline.addAll(childTimeline);
126        }
127
128        Collections.sort(timeline, sTimelineEntryComparator);
129
130        // Add end-event to timeline for all active children
131        NodeList activeChildrenAtEnd = par.getActiveChildrenAt(
132                (float) (endOffset - offset) * 1000);
133        for (int i = 0; i < activeChildrenAtEnd.getLength(); ++i) {
134            timeline.add(new TimelineEntry(endOffset,
135                    (ElementTime) activeChildrenAtEnd.item(i),
136                    TimelineEntry.ACTION_END));
137        }
138
139        // Set my end at last
140        timeline.add(myEnd);
141
142        return timeline;
143    }
144
145    private static ArrayList<TimelineEntry> getSeqTimeline(
146            ElementSequentialTimeContainer seq, double offset, double maxOffset) {
147        ArrayList<TimelineEntry> timeline = new ArrayList<TimelineEntry>();
148        double orgOffset = offset;
149
150        // Set my begin at first
151        TimeList myBeginList = seq.getBegin();
152        /*
153         * Begin list only contain 1 begin time which has been resolved.
154         * @see com.android.mms.dom.smil.ElementSequentialTimeContainerImpl#getBegin()
155         */
156        Time begin = myBeginList.item(0);
157        double beginOffset = begin.getResolvedOffset() + offset;
158        if (beginOffset > maxOffset) {
159            // This element can't be started.
160            return timeline;
161        }
162        TimelineEntry myBegin = new TimelineEntry(beginOffset, seq, TimelineEntry.ACTION_BEGIN);
163        timeline.add(myBegin);
164
165        TimeList myEndList = seq.getEnd();
166        /*
167         * End list only contain 1 end time which has been resolved.
168         * @see com.android.mms.dom.smil.ElementSequentialTimeContainerImpl#getEnd()
169         */
170        Time end = myEndList.item(0);
171        double endOffset = end.getResolvedOffset() + offset;
172        if (endOffset > maxOffset) {
173            endOffset = maxOffset;
174        }
175        TimelineEntry myEnd = new TimelineEntry(endOffset, seq, TimelineEntry.ACTION_END);
176
177        maxOffset = endOffset;
178
179        // Get children's timelines
180        NodeList children = seq.getTimeChildren();
181        for (int i = 0; i < children.getLength(); ++i) {
182            ElementTime child = (ElementTime) children.item(i);
183            ArrayList<TimelineEntry> childTimeline = getTimeline(child, offset, maxOffset);
184            timeline.addAll(childTimeline);
185
186            // Since the child timeline has been sorted, the offset of the last one is the biggest.
187            offset = childTimeline.get(childTimeline.size() - 1).getOffsetTime();
188        }
189
190        // Add end-event to timeline for all active children
191        NodeList activeChildrenAtEnd = seq.getActiveChildrenAt(
192                (float) (endOffset - orgOffset));
193        for (int i = 0; i < activeChildrenAtEnd.getLength(); ++i) {
194            timeline.add(new TimelineEntry(endOffset,
195                    (ElementTime) activeChildrenAtEnd.item(i),
196                    TimelineEntry.ACTION_END));
197        }
198
199        // Set my end at last
200        timeline.add(myEnd);
201
202        return timeline;
203    }
204
205    private static ArrayList<TimelineEntry> getTimeline(ElementTime element,
206            double offset, double maxOffset) {
207        if (element instanceof ElementParallelTimeContainer) {
208            return getParTimeline((ElementParallelTimeContainer) element, offset, maxOffset);
209        } else if (element instanceof ElementSequentialTimeContainer) {
210            return getSeqTimeline((ElementSequentialTimeContainer) element, offset, maxOffset);
211        } else {
212            // Not ElementTimeContainer here
213            ArrayList<TimelineEntry> timeline = new ArrayList<TimelineEntry>();
214
215            TimeList beginList = element.getBegin();
216            for (int i = 0; i < beginList.getLength(); ++i) {
217                Time begin = beginList.item(i);
218                if (begin.getResolved()) {
219                    double beginOffset = begin.getResolvedOffset() + offset;
220                    if (beginOffset <= maxOffset) {
221                        TimelineEntry entry = new TimelineEntry(beginOffset,
222                                element, TimelineEntry.ACTION_BEGIN);
223                        timeline.add(entry);
224                    }
225                }
226            }
227
228            TimeList endList = element.getEnd();
229            for (int i = 0; i < endList.getLength(); ++i) {
230                Time end = endList.item(i);
231                if (end.getResolved()) {
232                    double endOffset = end.getResolvedOffset() + offset;
233                    if (endOffset <= maxOffset) {
234                        TimelineEntry entry = new TimelineEntry(endOffset,
235                                element, TimelineEntry.ACTION_END);
236                        timeline.add(entry);
237                    }
238                }
239            }
240
241            Collections.sort(timeline, sTimelineEntryComparator);
242
243            return timeline;
244        }
245    }
246
247    private SmilPlayer() {
248        // Private constructor
249    }
250
251    public static SmilPlayer getPlayer() {
252        if (sPlayer == null) {
253            sPlayer = new SmilPlayer();
254        }
255        return sPlayer;
256    }
257
258    public synchronized boolean isPlayingState() {
259        return mState == SmilPlayerState.PLAYING;
260    }
261
262    public synchronized boolean isPlayedState() {
263        return mState == SmilPlayerState.PLAYED;
264    }
265
266    public synchronized boolean isPausedState() {
267        return mState == SmilPlayerState.PAUSED;
268    }
269
270    public synchronized boolean isStoppedState() {
271        return mState == SmilPlayerState.STOPPED;
272    }
273
274    private synchronized boolean isPauseAction() {
275        return mAction == SmilPlayerAction.PAUSE;
276    }
277
278    private synchronized boolean isStartAction() {
279        return mAction == SmilPlayerAction.START;
280    }
281
282    private synchronized boolean isStopAction() {
283        return mAction == SmilPlayerAction.STOP;
284    }
285
286    private synchronized boolean isReloadAction() {
287        return mAction == SmilPlayerAction.RELOAD;
288    }
289
290    private synchronized boolean isNextAction() {
291      return mAction == SmilPlayerAction.NEXT;
292    }
293
294    private synchronized boolean isPrevAction() {
295      return mAction == SmilPlayerAction.PREV;
296    }
297
298    public synchronized void init(ElementTime root) {
299        mRoot = root;
300        mAllEntries = getTimeline(mRoot, 0, Long.MAX_VALUE);
301        mMediaTimeUpdatedEvent = ((DocumentEvent) mRoot).createEvent("Event");
302        mMediaTimeUpdatedEvent.initEvent(MEDIA_TIME_UPDATED_EVENT, false, false);
303        mActiveElements = new ArrayList<ElementTime>();
304    }
305
306    public synchronized void play() {
307        if (!isPlayingState()) {
308            mCurrentTime = 0;
309            mCurrentElement = 0;
310            mCurrentSlide = 0;
311            mPlayerThread = new Thread(this, "SmilPlayer thread");
312            mState = SmilPlayerState.PLAYING;
313            mPlayerThread.start();
314        } else {
315            Log.w(TAG, "Error State: Playback is playing!");
316        }
317    }
318
319    public synchronized void pause() {
320        if (isPlayingState()) {
321            mAction = SmilPlayerAction.PAUSE;
322            notifyAll();
323        } else {
324            Log.w(TAG, "Error State: Playback is not playing!");
325        }
326    }
327
328    public synchronized void start() {
329        if (isPausedState()) {
330            resumeActiveElements();
331            mAction = SmilPlayerAction.START;
332            notifyAll();
333        } else if (isPlayedState()) {
334            play();
335        } else {
336            Log.w(TAG, "Error State: Playback can not be started!");
337        }
338    }
339
340    public synchronized void stop() {
341        if (isPlayingState() || isPausedState()) {
342            mAction = SmilPlayerAction.STOP;
343            notifyAll();
344        } else if (isPlayedState()) {
345            actionStop();
346        }
347    }
348
349    public synchronized void stopWhenReload() {
350        endActiveElements();
351    }
352
353    public synchronized void reload() {
354        if (isPlayingState() || isPausedState()) {
355            mAction = SmilPlayerAction.RELOAD;
356            notifyAll();
357        } else if (isPlayedState()) {
358            actionReload();
359        }
360    }
361
362    public synchronized void next() {
363      if (isPlayingState() || isPausedState()) {
364        mAction = SmilPlayerAction.NEXT;
365        notifyAll();
366      }
367    }
368
369    public synchronized void prev() {
370      if (isPlayingState() || isPausedState()) {
371        mAction = SmilPlayerAction.PREV;
372        notifyAll();
373      }
374    }
375
376    private synchronized boolean isBeginOfSlide(TimelineEntry entry) {
377        return (TimelineEntry.ACTION_BEGIN == entry.getAction())
378                    && (entry.getElement() instanceof SmilParElementImpl);
379    }
380
381    private synchronized void reloadActiveSlide() {
382        mActiveElements.clear();
383        beginSmilDocument();
384
385        for (int i = mCurrentSlide; i < mCurrentElement; i++) {
386            TimelineEntry entry = mAllEntries.get(i);
387            actionEntry(entry);
388        }
389        seekActiveMedia();
390    }
391
392    private synchronized void beginSmilDocument() {
393        TimelineEntry entry = mAllEntries.get(0);
394        actionEntry(entry);
395    }
396
397    private synchronized double getOffsetTime(ElementTime element) {
398        for (int i = mCurrentSlide; i < mCurrentElement; i++) {
399            TimelineEntry entry = mAllEntries.get(i);
400            if (element.equals(entry.getElement())) {
401                return entry.getOffsetTime() * 1000;  // in ms
402            }
403        }
404        return -1;
405    }
406
407    private synchronized void seekActiveMedia() {
408        for (int i = mActiveElements.size() - 1; i >= 0; i--) {
409            ElementTime element = mActiveElements.get(i);
410            if (element instanceof SmilParElementImpl) {
411                return;
412            }
413            double offset = getOffsetTime(element);
414            if ((offset >= 0) && (offset <= mCurrentTime)) {
415                if (LOCAL_LOGV) {
416                    Log.v(TAG, "[SEEK]  " + " at " + mCurrentTime
417                            + " " + element);
418                }
419                element.seekElement( (float) (mCurrentTime - offset) );
420            }
421        }
422    }
423
424    private synchronized void waitForEntry(long interval)
425            throws InterruptedException {
426        if (LOCAL_LOGV) {
427            Log.v(TAG, "Waiting for " + interval + "ms.");
428        }
429
430        long overhead = 0;
431
432        while (interval > 0) {
433            long startAt = System.currentTimeMillis();
434            long sleep = Math.min(interval, TIMESLICE);
435            if (overhead < sleep) {
436                wait(sleep - overhead);
437                mCurrentTime += sleep;
438            } else {
439                sleep = 0;
440                mCurrentTime += overhead;
441            }
442
443            if (isStopAction() || isReloadAction() || isPauseAction() || isNextAction() ||
444                isPrevAction()) {
445                return;
446            }
447
448            ((EventTarget) mRoot).dispatchEvent(mMediaTimeUpdatedEvent);
449
450            interval -= TIMESLICE;
451            overhead = System.currentTimeMillis() - startAt - sleep;
452        }
453    }
454
455    public synchronized int getDuration() {
456         if ((mAllEntries != null) && !mAllEntries.isEmpty()) {
457             return (int) mAllEntries.get(mAllEntries.size() - 1).mOffsetTime * 1000;
458         }
459         return 0;
460    }
461
462    public synchronized int getCurrentPosition() {
463        return (int) mCurrentTime;
464    }
465
466    private synchronized void endActiveElements() {
467        for (int i = mActiveElements.size() - 1; i >= 0; i--) {
468            ElementTime element = mActiveElements.get(i);
469            if (LOCAL_LOGV) {
470                Log.v(TAG, "[STOP]  " + " at " + mCurrentTime
471                        + " " + element);
472            }
473            element.endElement();
474        }
475    }
476
477    private synchronized void pauseActiveElements() {
478        for (int i = mActiveElements.size() - 1; i >= 0; i--) {
479            ElementTime element = mActiveElements.get(i);
480            if (LOCAL_LOGV) {
481                Log.v(TAG, "[PAUSE]  " + " at " + mCurrentTime
482                        + " " + element);
483            }
484            element.pauseElement();
485        }
486    }
487
488    private synchronized void resumeActiveElements() {
489        int size = mActiveElements.size();
490        for (int i = 0; i < size; i++) {
491            ElementTime element = mActiveElements.get(i);
492            if (LOCAL_LOGV) {
493                Log.v(TAG, "[RESUME]  " + " at " + mCurrentTime
494                        + " " + element);
495            }
496            element.resumeElement();
497        }
498    }
499
500    private synchronized void waitForWakeUp() {
501        try {
502            while ( !(isStartAction() || isStopAction() || isReloadAction() ||
503                    isNextAction() || isPrevAction()) ) {
504                wait(TIMESLICE);
505            }
506            if (isStartAction()) {
507                mAction = SmilPlayerAction.NO_ACTIVE_ACTION;
508                mState = SmilPlayerState.PLAYING;
509            }
510        } catch (InterruptedException e) {
511            Log.e(TAG, "Unexpected InterruptedException.", e);
512        }
513    }
514
515    private synchronized void actionEntry(TimelineEntry entry) {
516        switch (entry.getAction()) {
517            case TimelineEntry.ACTION_BEGIN:
518                if (LOCAL_LOGV) {
519                    Log.v(TAG, "[START] " + " at " + mCurrentTime + " "
520                            + entry.getElement());
521                }
522                entry.getElement().beginElement();
523                mActiveElements.add(entry.getElement());
524                break;
525            case TimelineEntry.ACTION_END:
526                if (LOCAL_LOGV) {
527                    Log.v(TAG, "[STOP]  " + " at " + mCurrentTime + " "
528                            + entry.getElement());
529                }
530                entry.getElement().endElement();
531                mActiveElements.remove(entry.getElement());
532                break;
533            default:
534                break;
535        }
536    }
537
538    private synchronized TimelineEntry reloadCurrentEntry() {
539        // Check if the position is less than size of all entries
540        if (mCurrentElement < mAllEntries.size()) {
541            return mAllEntries.get(mCurrentElement);
542        } else {
543            return null;
544        }
545    }
546
547    private void stopCurrentSlide() {
548        HashSet<TimelineEntry> skippedEntries = new HashSet<TimelineEntry>();
549        int totalEntries = mAllEntries.size();
550        for (int i = mCurrentElement; i < totalEntries; i++) {
551            // Stop any started entries, and skip the not started entries until
552            // meeting the end of slide
553            TimelineEntry entry = mAllEntries.get(i);
554            int action = entry.getAction();
555            if (entry.getElement() instanceof SmilParElementImpl &&
556                    action == TimelineEntry.ACTION_END) {
557                actionEntry(entry);
558                mCurrentElement = i;
559                break;
560            } else if (action == TimelineEntry.ACTION_END && !skippedEntries.contains(entry)) {
561                    actionEntry(entry);
562            } else if (action == TimelineEntry.ACTION_BEGIN) {
563                skippedEntries.add(entry);
564            }
565        }
566    }
567
568    private TimelineEntry loadNextSlide() {
569      TimelineEntry entry;
570      int totalEntries = mAllEntries.size();
571      for (int i = mCurrentElement; i < totalEntries; i++) {
572          entry = mAllEntries.get(i);
573          if (isBeginOfSlide(entry)) {
574              mCurrentElement = i;
575              mCurrentSlide = i;
576              mCurrentTime = (long)(entry.getOffsetTime() * 1000);
577              return entry;
578          }
579      }
580      // No slide, finish play back
581      mCurrentElement++;
582      entry = null;
583      if (mCurrentElement < totalEntries) {
584          entry = mAllEntries.get(mCurrentElement);
585          mCurrentTime = (long)(entry.getOffsetTime() * 1000);
586      }
587      return entry;
588    }
589
590    private TimelineEntry loadPrevSlide() {
591      int skippedSlides = 1;
592      int latestBeginEntryIndex = -1;
593      for (int i = mCurrentSlide; i >= 0; i--) {
594        TimelineEntry entry = mAllEntries.get(i);
595        if (isBeginOfSlide(entry)) {
596            latestBeginEntryIndex = i;
597          if (0 == skippedSlides-- ) {
598            mCurrentElement = i;
599            mCurrentSlide = i;
600            mCurrentTime = (long)(entry.getOffsetTime() * 1000);
601            return entry;
602          }
603        }
604      }
605      if (latestBeginEntryIndex != -1) {
606          mCurrentElement = latestBeginEntryIndex;
607          mCurrentSlide = latestBeginEntryIndex;
608          return mAllEntries.get(mCurrentElement);
609      }
610      return null;
611    }
612
613    private synchronized TimelineEntry actionNext() {
614        stopCurrentSlide();
615        return loadNextSlide();
616   }
617
618    private synchronized TimelineEntry actionPrev() {
619        stopCurrentSlide();
620        return loadPrevSlide();
621    }
622
623    private synchronized void actionPause() {
624        pauseActiveElements();
625        mState = SmilPlayerState.PAUSED;
626        mAction = SmilPlayerAction.NO_ACTIVE_ACTION;
627    }
628
629    private synchronized void actionStop() {
630        endActiveElements();
631        mCurrentTime = 0;
632        mCurrentElement = 0;
633        mCurrentSlide = 0;
634        mState = SmilPlayerState.STOPPED;
635        mAction = SmilPlayerAction.NO_ACTIVE_ACTION;
636    }
637
638    private synchronized void actionReload() {
639        reloadActiveSlide();
640        mAction = SmilPlayerAction.NO_ACTIVE_ACTION;
641    }
642
643    public void run() {
644        if (isStoppedState()) {
645            return;
646        }
647        if (LOCAL_LOGV) {
648            dumpAllEntries();
649        }
650        // Play the Element by following the timeline
651        int size = mAllEntries.size();
652        for (mCurrentElement = 0; mCurrentElement < size; mCurrentElement++) {
653            TimelineEntry entry = mAllEntries.get(mCurrentElement);
654            if (isBeginOfSlide(entry)) {
655                mCurrentSlide = mCurrentElement;
656            }
657            long offset = (long) (entry.getOffsetTime() * 1000); // in ms.
658            while (offset > mCurrentTime) {
659                try {
660                    waitForEntry(offset - mCurrentTime);
661                } catch (InterruptedException e) {
662                    Log.e(TAG, "Unexpected InterruptedException.", e);
663                }
664
665                while (isPauseAction() || isStopAction() || isReloadAction() || isNextAction() ||
666                    isPrevAction()) {
667                    if (isPauseAction()) {
668                        actionPause();
669                        waitForWakeUp();
670                    }
671
672                    if (isStopAction()) {
673                        actionStop();
674                        return;
675                    }
676
677                    if (isReloadAction()) {
678                        actionReload();
679                        entry = reloadCurrentEntry();
680                        if (entry == null)
681                            return;
682                        if (isPausedState()) {
683                            mAction = SmilPlayerAction.PAUSE;
684                        }
685                    }
686
687                    if (isNextAction()) {
688                        TimelineEntry nextEntry = actionNext();
689                        if (nextEntry != null) {
690                            entry = nextEntry;
691                        }
692                        if (mState == SmilPlayerState.PAUSED) {
693                            mAction = SmilPlayerAction.PAUSE;
694                            actionEntry(entry);
695                        } else {
696                            mAction = SmilPlayerAction.NO_ACTIVE_ACTION;
697                        }
698                        offset = mCurrentTime;
699                    }
700
701                    if (isPrevAction()) {
702                        TimelineEntry prevEntry = actionPrev();
703                        if (prevEntry != null) {
704                            entry = prevEntry;
705                        }
706                        if (mState == SmilPlayerState.PAUSED) {
707                            mAction = SmilPlayerAction.PAUSE;
708                            actionEntry(entry);
709                        } else {
710                            mAction = SmilPlayerAction.NO_ACTIVE_ACTION;
711                        }
712                        offset = mCurrentTime;
713                    }
714                }
715            }
716            mCurrentTime = offset;
717            actionEntry(entry);
718        }
719
720        mState = SmilPlayerState.PLAYED;
721    }
722
723    private static final class TimelineEntry {
724        final static int ACTION_BEGIN = 0;
725        final static int ACTION_END   = 1;
726
727        private final double mOffsetTime;
728        private final ElementTime mElement;
729        private final int mAction;
730
731        public TimelineEntry(double offsetTime, ElementTime element, int action) {
732            mOffsetTime = offsetTime;
733            mElement = element;
734            mAction  = action;
735        }
736
737        public double getOffsetTime() {
738            return mOffsetTime;
739        }
740
741        public ElementTime getElement() {
742            return mElement;
743        }
744
745        public int getAction() {
746            return mAction;
747        }
748
749        public String toString() {
750            return "Type = " + mElement + " offset = " + getOffsetTime() + " action = " + getAction();
751        }
752    }
753
754    private void dumpAllEntries() {
755        if (LOCAL_LOGV) {
756            for (TimelineEntry entry : mAllEntries) {
757                Log.v(TAG, "[Entry] "+ entry);
758            }
759        }
760    }
761}
762