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