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 18 package com.android.mms.dom.smil; 19 20 import org.w3c.dom.NodeList; 21 import org.w3c.dom.events.DocumentEvent; 22 import org.w3c.dom.events.Event; 23 import org.w3c.dom.events.EventTarget; 24 import org.w3c.dom.smil.ElementParallelTimeContainer; 25 import org.w3c.dom.smil.ElementSequentialTimeContainer; 26 import org.w3c.dom.smil.ElementTime; 27 import org.w3c.dom.smil.Time; 28 import org.w3c.dom.smil.TimeList; 29 30 import android.util.Config; 31 import android.util.Log; 32 33 import java.util.ArrayList; 34 import java.util.Collections; 35 import java.util.Comparator; 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 */ 42 public 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 = DEBUG ? Config.LOGD : Config.LOGV; 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 } 63 64 public static final String MEDIA_TIME_UPDATED_EVENT = "mediaTimeUpdated"; 65 66 private static final Comparator<TimelineEntry> sTimelineEntryComparator = 67 new Comparator<TimelineEntry>() { 68 public int compare(TimelineEntry o1, TimelineEntry o2) { 69 return Double.compare(o1.getOffsetTime(), o2.getOffsetTime()); 70 } 71 }; 72 73 private static SmilPlayer sPlayer; 74 75 private long mCurrentTime; 76 private int mCurrentElement; 77 private int mCurrentSlide; 78 private ArrayList<TimelineEntry> mAllEntries; 79 private ElementTime mRoot; 80 private Thread mPlayerThread; 81 private SmilPlayerState mState = SmilPlayerState.INITIALIZED; 82 private SmilPlayerAction mAction = SmilPlayerAction.NO_ACTIVE_ACTION; 83 private ArrayList<ElementTime> mActiveElements; 84 private Event mMediaTimeUpdatedEvent; 85 getParTimeline( ElementParallelTimeContainer par, double offset, double maxOffset)86 private static ArrayList<TimelineEntry> getParTimeline( 87 ElementParallelTimeContainer par, double offset, double maxOffset) { 88 ArrayList<TimelineEntry> timeline = new ArrayList<TimelineEntry>(); 89 90 // Set my begin at first 91 TimeList myBeginList = par.getBegin(); 92 /* 93 * Begin list only contain 1 begin time which has been resolved. 94 * @see com.android.mms.dom.smil.ElementParallelTimeContainerImpl#getBegin() 95 */ 96 Time begin = myBeginList.item(0); 97 double beginOffset = begin.getResolvedOffset() + offset; 98 if (beginOffset > maxOffset) { 99 // This element can't be started. 100 return timeline; 101 } 102 TimelineEntry myBegin = new TimelineEntry(beginOffset, par, TimelineEntry.ACTION_BEGIN); 103 timeline.add(myBegin); 104 105 TimeList myEndList = par.getEnd(); 106 /* 107 * End list only contain 1 end time which has been resolved. 108 * @see com.android.mms.dom.smil.ElementParallelTimeContainerImpl#getEnd() 109 */ 110 Time end = myEndList.item(0); 111 double endOffset = end.getResolvedOffset() + offset; 112 if (endOffset > maxOffset) { 113 endOffset = maxOffset; 114 } 115 TimelineEntry myEnd = new TimelineEntry(endOffset, par, TimelineEntry.ACTION_END); 116 117 maxOffset = endOffset; 118 119 NodeList children = par.getTimeChildren(); 120 for (int i = 0; i < children.getLength(); ++i) { 121 ElementTime child = (ElementTime) children.item(i); 122 ArrayList<TimelineEntry> childTimeline = getTimeline(child, offset, maxOffset); 123 timeline.addAll(childTimeline); 124 } 125 126 Collections.sort(timeline, sTimelineEntryComparator); 127 128 // Add end-event to timeline for all active children 129 NodeList activeChildrenAtEnd = par.getActiveChildrenAt( 130 (float) (endOffset - offset) * 1000); 131 for (int i = 0; i < activeChildrenAtEnd.getLength(); ++i) { 132 timeline.add(new TimelineEntry(endOffset, 133 (ElementTime) activeChildrenAtEnd.item(i), 134 TimelineEntry.ACTION_END)); 135 } 136 137 // Set my end at last 138 timeline.add(myEnd); 139 140 return timeline; 141 } 142 getSeqTimeline( ElementSequentialTimeContainer seq, double offset, double maxOffset)143 private static ArrayList<TimelineEntry> getSeqTimeline( 144 ElementSequentialTimeContainer seq, double offset, double maxOffset) { 145 ArrayList<TimelineEntry> timeline = new ArrayList<TimelineEntry>(); 146 double orgOffset = offset; 147 148 // Set my begin at first 149 TimeList myBeginList = seq.getBegin(); 150 /* 151 * Begin list only contain 1 begin time which has been resolved. 152 * @see com.android.mms.dom.smil.ElementSequentialTimeContainerImpl#getBegin() 153 */ 154 Time begin = myBeginList.item(0); 155 double beginOffset = begin.getResolvedOffset() + offset; 156 if (beginOffset > maxOffset) { 157 // This element can't be started. 158 return timeline; 159 } 160 TimelineEntry myBegin = new TimelineEntry(beginOffset, seq, TimelineEntry.ACTION_BEGIN); 161 timeline.add(myBegin); 162 163 TimeList myEndList = seq.getEnd(); 164 /* 165 * End list only contain 1 end time which has been resolved. 166 * @see com.android.mms.dom.smil.ElementSequentialTimeContainerImpl#getEnd() 167 */ 168 Time end = myEndList.item(0); 169 double endOffset = end.getResolvedOffset() + offset; 170 if (endOffset > maxOffset) { 171 endOffset = maxOffset; 172 } 173 TimelineEntry myEnd = new TimelineEntry(endOffset, seq, TimelineEntry.ACTION_END); 174 175 maxOffset = endOffset; 176 177 // Get children's timelines 178 NodeList children = seq.getTimeChildren(); 179 for (int i = 0; i < children.getLength(); ++i) { 180 ElementTime child = (ElementTime) children.item(i); 181 ArrayList<TimelineEntry> childTimeline = getTimeline(child, offset, maxOffset); 182 timeline.addAll(childTimeline); 183 184 // Since the child timeline has been sorted, the offset of the last one is the biggest. 185 offset = childTimeline.get(childTimeline.size() - 1).getOffsetTime(); 186 } 187 188 // Add end-event to timeline for all active children 189 NodeList activeChildrenAtEnd = seq.getActiveChildrenAt( 190 (float) (endOffset - orgOffset)); 191 for (int i = 0; i < activeChildrenAtEnd.getLength(); ++i) { 192 timeline.add(new TimelineEntry(endOffset, 193 (ElementTime) activeChildrenAtEnd.item(i), 194 TimelineEntry.ACTION_END)); 195 } 196 197 // Set my end at last 198 timeline.add(myEnd); 199 200 return timeline; 201 } 202 getTimeline(ElementTime element, double offset, double maxOffset)203 private static ArrayList<TimelineEntry> getTimeline(ElementTime element, 204 double offset, double maxOffset) { 205 if (element instanceof ElementParallelTimeContainer) { 206 return getParTimeline((ElementParallelTimeContainer) element, offset, maxOffset); 207 } else if (element instanceof ElementSequentialTimeContainer) { 208 return getSeqTimeline((ElementSequentialTimeContainer) element, offset, maxOffset); 209 } else { 210 // Not ElementTimeContainer here 211 ArrayList<TimelineEntry> timeline = new ArrayList<TimelineEntry>(); 212 213 TimeList beginList = element.getBegin(); 214 for (int i = 0; i < beginList.getLength(); ++i) { 215 Time begin = beginList.item(i); 216 if (begin.getResolved()) { 217 double beginOffset = begin.getResolvedOffset() + offset; 218 if (beginOffset <= maxOffset) { 219 TimelineEntry entry = new TimelineEntry(beginOffset, 220 element, TimelineEntry.ACTION_BEGIN); 221 timeline.add(entry); 222 } 223 } 224 } 225 226 TimeList endList = element.getEnd(); 227 for (int i = 0; i < endList.getLength(); ++i) { 228 Time end = endList.item(i); 229 if (end.getResolved()) { 230 double endOffset = end.getResolvedOffset() + offset; 231 if (endOffset <= maxOffset) { 232 TimelineEntry entry = new TimelineEntry(endOffset, 233 element, TimelineEntry.ACTION_END); 234 timeline.add(entry); 235 } 236 } 237 } 238 239 Collections.sort(timeline, sTimelineEntryComparator); 240 241 return timeline; 242 } 243 } 244 SmilPlayer()245 private SmilPlayer() { 246 // Private constructor 247 } 248 getPlayer()249 public static SmilPlayer getPlayer() { 250 if (sPlayer == null) { 251 sPlayer = new SmilPlayer(); 252 } 253 return sPlayer; 254 } 255 isPlayingState()256 public synchronized boolean isPlayingState() { 257 return mState == SmilPlayerState.PLAYING; 258 } 259 isPlayedState()260 public synchronized boolean isPlayedState() { 261 return mState == SmilPlayerState.PLAYED; 262 } 263 isPausedState()264 public synchronized boolean isPausedState() { 265 return mState == SmilPlayerState.PAUSED; 266 } 267 isStoppedState()268 public synchronized boolean isStoppedState() { 269 return mState == SmilPlayerState.STOPPED; 270 } 271 isPauseAction()272 private synchronized boolean isPauseAction() { 273 return mAction == SmilPlayerAction.PAUSE; 274 } 275 isStartAction()276 private synchronized boolean isStartAction() { 277 return mAction == SmilPlayerAction.START; 278 } 279 isStopAction()280 private synchronized boolean isStopAction() { 281 return mAction == SmilPlayerAction.STOP; 282 } 283 isReloadAction()284 private synchronized boolean isReloadAction() { 285 return mAction == SmilPlayerAction.RELOAD; 286 } 287 init(ElementTime root)288 public synchronized void init(ElementTime root) { 289 mRoot = root; 290 mAllEntries = getTimeline(mRoot, 0, Long.MAX_VALUE); 291 mMediaTimeUpdatedEvent = ((DocumentEvent) mRoot).createEvent("Event"); 292 mMediaTimeUpdatedEvent.initEvent(MEDIA_TIME_UPDATED_EVENT, false, false); 293 mActiveElements = new ArrayList<ElementTime>(); 294 } 295 play()296 public synchronized void play() { 297 if (!isPlayingState()) { 298 mCurrentTime = 0; 299 mCurrentElement = 0; 300 mCurrentSlide = 0; 301 mPlayerThread = new Thread(this); 302 mState = SmilPlayerState.PLAYING; 303 mPlayerThread.start(); 304 } else { 305 Log.w(TAG, "Error State: Playback is playing!"); 306 } 307 } 308 pause()309 public synchronized void pause() { 310 if (isPlayingState()) { 311 mAction = SmilPlayerAction.PAUSE; 312 notifyAll(); 313 } else { 314 Log.w(TAG, "Error State: Playback is not playing!"); 315 } 316 } 317 start()318 public synchronized void start() { 319 if (isPausedState()) { 320 resumeActiveElements(); 321 mAction = SmilPlayerAction.START; 322 notifyAll(); 323 } else if (isPlayedState()) { 324 play(); 325 } else { 326 Log.w(TAG, "Error State: Playback can not be started!"); 327 } 328 } 329 stop()330 public synchronized void stop() { 331 if (isPlayingState() || isPausedState()) { 332 mAction = SmilPlayerAction.STOP; 333 notifyAll(); 334 } else if (isPlayedState()) { 335 actionStop(); 336 } 337 } 338 stopWhenReload()339 public synchronized void stopWhenReload() { 340 endActiveElements(); 341 } 342 reload()343 public synchronized void reload() { 344 if (isPlayingState() || isPausedState()) { 345 mAction = SmilPlayerAction.RELOAD; 346 notifyAll(); 347 } else if (isPlayedState()) { 348 actionReload(); 349 } 350 } 351 isBeginOfSlide(TimelineEntry entry)352 private synchronized boolean isBeginOfSlide(TimelineEntry entry) { 353 return (TimelineEntry.ACTION_BEGIN == entry.getAction()) 354 && (entry.getElement() instanceof SmilParElementImpl); 355 } 356 reloadActiveSlide()357 private synchronized void reloadActiveSlide() { 358 mActiveElements.clear(); 359 beginSmilDocument(); 360 361 for (int i = mCurrentSlide; i < mCurrentElement; i++) { 362 TimelineEntry entry = mAllEntries.get(i); 363 actionEntry(entry); 364 } 365 seekActiveMedia(); 366 } 367 beginSmilDocument()368 private synchronized void beginSmilDocument() { 369 TimelineEntry entry = mAllEntries.get(0); 370 actionEntry(entry); 371 } 372 getOffsetTime(ElementTime element)373 private synchronized double getOffsetTime(ElementTime element) { 374 for (int i = mCurrentSlide; i < mCurrentElement; i++) { 375 TimelineEntry entry = mAllEntries.get(i); 376 if (element.equals(entry.getElement())) { 377 return entry.getOffsetTime() * 1000; // in ms 378 } 379 } 380 return -1; 381 } 382 seekActiveMedia()383 private synchronized void seekActiveMedia() { 384 for (int i = mActiveElements.size() - 1; i >= 0; i--) { 385 ElementTime element = mActiveElements.get(i); 386 if (element instanceof SmilParElementImpl) { 387 return; 388 } 389 double offset = getOffsetTime(element); 390 if ((offset >= 0) && (offset <= mCurrentTime)) { 391 if (LOCAL_LOGV) { 392 Log.v(TAG, "[SEEK] " + " at " + mCurrentTime 393 + " " + element); 394 } 395 element.seekElement( (float) (mCurrentTime - offset) ); 396 } 397 } 398 } 399 waitForEntry(long interval)400 private synchronized void waitForEntry(long interval) 401 throws InterruptedException { 402 if (LOCAL_LOGV) { 403 Log.v(TAG, "Waiting for " + interval + "ms."); 404 } 405 406 long overhead = 0; 407 408 while (interval > 0) { 409 long startAt = System.currentTimeMillis(); 410 long sleep = Math.min(interval, TIMESLICE); 411 if (overhead < sleep) { 412 wait(sleep - overhead); 413 mCurrentTime += sleep; 414 } else { 415 sleep = 0; 416 mCurrentTime += overhead; 417 } 418 419 if (isStopAction() || isReloadAction() || isPauseAction()) { 420 return; 421 } 422 423 ((EventTarget) mRoot).dispatchEvent(mMediaTimeUpdatedEvent); 424 425 interval -= TIMESLICE; 426 overhead = System.currentTimeMillis() - startAt - sleep; 427 } 428 } 429 getDuration()430 public synchronized int getDuration() { 431 if ((mAllEntries != null) && !mAllEntries.isEmpty()) { 432 return (int) mAllEntries.get(mAllEntries.size() - 1).mOffsetTime * 1000; 433 } 434 return 0; 435 } 436 getCurrentPosition()437 public synchronized int getCurrentPosition() { 438 return (int) mCurrentTime; 439 } 440 endActiveElements()441 private synchronized void endActiveElements() { 442 for (int i = mActiveElements.size() - 1; i >= 0; i--) { 443 ElementTime element = mActiveElements.get(i); 444 if (LOCAL_LOGV) { 445 Log.v(TAG, "[STOP] " + " at " + mCurrentTime 446 + " " + element); 447 } 448 element.endElement(); 449 } 450 } 451 pauseActiveElements()452 private synchronized void pauseActiveElements() { 453 for (int i = mActiveElements.size() - 1; i >= 0; i--) { 454 ElementTime element = mActiveElements.get(i); 455 if (LOCAL_LOGV) { 456 Log.v(TAG, "[PAUSE] " + " at " + mCurrentTime 457 + " " + element); 458 } 459 element.pauseElement(); 460 } 461 } 462 resumeActiveElements()463 private synchronized void resumeActiveElements() { 464 int size = mActiveElements.size(); 465 for (int i = 0; i < size; i++) { 466 ElementTime element = mActiveElements.get(i); 467 if (LOCAL_LOGV) { 468 Log.v(TAG, "[RESUME] " + " at " + mCurrentTime 469 + " " + element); 470 } 471 element.resumeElement(); 472 } 473 } 474 waitForWakeUp()475 private synchronized void waitForWakeUp() { 476 try { 477 while ( !(isStartAction() || isStopAction() || isReloadAction()) ) { 478 wait(TIMESLICE); 479 } 480 if (isStartAction()) { 481 mAction = SmilPlayerAction.NO_ACTIVE_ACTION; 482 mState = SmilPlayerState.PLAYING; 483 } 484 } catch (InterruptedException e) { 485 Log.e(TAG, "Unexpected InterruptedException.", e); 486 } 487 } 488 actionEntry(TimelineEntry entry)489 private synchronized void actionEntry(TimelineEntry entry) { 490 switch (entry.getAction()) { 491 case TimelineEntry.ACTION_BEGIN: 492 if (LOCAL_LOGV) { 493 Log.v(TAG, "[START] " + " at " + mCurrentTime + " " 494 + entry.getElement()); 495 } 496 entry.getElement().beginElement(); 497 mActiveElements.add(entry.getElement()); 498 break; 499 case TimelineEntry.ACTION_END: 500 if (LOCAL_LOGV) { 501 Log.v(TAG, "[STOP] " + " at " + mCurrentTime + " " 502 + entry.getElement()); 503 } 504 entry.getElement().endElement(); 505 mActiveElements.remove(entry.getElement()); 506 break; 507 default: 508 break; 509 } 510 } 511 reloadCurrentEntry()512 private synchronized TimelineEntry reloadCurrentEntry() { 513 return mAllEntries.get(mCurrentElement); 514 } 515 actionPause()516 private synchronized void actionPause() { 517 pauseActiveElements(); 518 mState = SmilPlayerState.PAUSED; 519 mAction = SmilPlayerAction.NO_ACTIVE_ACTION; 520 } 521 actionStop()522 private synchronized void actionStop() { 523 endActiveElements(); 524 mCurrentTime = 0; 525 mCurrentElement = 0; 526 mCurrentSlide = 0; 527 mState = SmilPlayerState.STOPPED; 528 mAction = SmilPlayerAction.NO_ACTIVE_ACTION; 529 } 530 actionReload()531 private synchronized void actionReload() { 532 reloadActiveSlide(); 533 mAction = SmilPlayerAction.NO_ACTIVE_ACTION; 534 } 535 run()536 public void run() { 537 if (isStoppedState()) { 538 return; 539 } 540 541 // Play the Element by following the timeline 542 int size = mAllEntries.size(); 543 for (mCurrentElement = 0; mCurrentElement < size; mCurrentElement++) { 544 TimelineEntry entry = mAllEntries.get(mCurrentElement); 545 if (isBeginOfSlide(entry)) { 546 mCurrentSlide = mCurrentElement; 547 } 548 long offset = (long) (entry.getOffsetTime() * 1000); // in ms. 549 while (offset > mCurrentTime) { 550 try { 551 waitForEntry(offset - mCurrentTime); 552 } catch (InterruptedException e) { 553 Log.e(TAG, "Unexpected InterruptedException.", e); 554 } 555 556 while (isPauseAction() || isStopAction() || isReloadAction()) { 557 if (isPauseAction()) { 558 actionPause(); 559 waitForWakeUp(); 560 } 561 562 if (isStopAction()) { 563 actionStop(); 564 return; 565 } 566 567 if (isReloadAction()) { 568 actionReload(); 569 entry = reloadCurrentEntry(); 570 if (isPausedState()) { 571 mAction = SmilPlayerAction.PAUSE; 572 } 573 } 574 } 575 } 576 mCurrentTime = offset; 577 actionEntry(entry); 578 } 579 580 mState = SmilPlayerState.PLAYED; 581 } 582 583 private static final class TimelineEntry { 584 final static int ACTION_BEGIN = 0; 585 final static int ACTION_END = 1; 586 587 private final double mOffsetTime; 588 private final ElementTime mElement; 589 private final int mAction; 590 TimelineEntry(double offsetTime, ElementTime element, int action)591 public TimelineEntry(double offsetTime, ElementTime element, int action) { 592 mOffsetTime = offsetTime; 593 mElement = element; 594 mAction = action; 595 } 596 getOffsetTime()597 public double getOffsetTime() { 598 return mOffsetTime; 599 } 600 getElement()601 public ElementTime getElement() { 602 return mElement; 603 } 604 getAction()605 public int getAction() { 606 return mAction; 607 } 608 } 609 } 610