1 /* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.google.android.exoplayer2.source.dash; 17 18 import static com.google.android.exoplayer2.util.Util.parseXsDateTime; 19 20 import android.os.Handler; 21 import android.os.Message; 22 import androidx.annotation.Nullable; 23 import com.google.android.exoplayer2.C; 24 import com.google.android.exoplayer2.Format; 25 import com.google.android.exoplayer2.FormatHolder; 26 import com.google.android.exoplayer2.ParserException; 27 import com.google.android.exoplayer2.drm.DrmSessionManager; 28 import com.google.android.exoplayer2.extractor.TrackOutput; 29 import com.google.android.exoplayer2.metadata.Metadata; 30 import com.google.android.exoplayer2.metadata.MetadataInputBuffer; 31 import com.google.android.exoplayer2.metadata.emsg.EventMessage; 32 import com.google.android.exoplayer2.metadata.emsg.EventMessageDecoder; 33 import com.google.android.exoplayer2.source.SampleQueue; 34 import com.google.android.exoplayer2.source.chunk.Chunk; 35 import com.google.android.exoplayer2.source.dash.manifest.DashManifest; 36 import com.google.android.exoplayer2.upstream.Allocator; 37 import com.google.android.exoplayer2.upstream.DataReader; 38 import com.google.android.exoplayer2.util.MediaSourceEventDispatcher; 39 import com.google.android.exoplayer2.util.ParsableByteArray; 40 import com.google.android.exoplayer2.util.Util; 41 import java.io.IOException; 42 import java.util.Iterator; 43 import java.util.Map; 44 import java.util.TreeMap; 45 46 /** 47 * Handles all emsg messages from all media tracks for the player. 48 * 49 * <p>This class will only respond to emsg messages which have schemeIdUri 50 * "urn:mpeg:dash:event:2012", and value "1"/"2"/"3". When it encounters one of these messages, it 51 * will handle the message according to Section 4.5.2.1 DASH -IF IOP Version 4.1: 52 * 53 * <ul> 54 * <li>If both presentation time delta and event duration are zero, it means the media 55 * presentation has ended. 56 * <li>Else, it will parse the message data from the emsg message to find the publishTime of the 57 * expired manifest, and mark manifest with publishTime smaller than that values to be 58 * expired. 59 * </ul> 60 * 61 * In both cases, the DASH media source will be notified, and a manifest reload should be triggered. 62 */ 63 public final class PlayerEmsgHandler implements Handler.Callback { 64 65 private static final int EMSG_MANIFEST_EXPIRED = 1; 66 67 /** Callbacks for player emsg events encountered during DASH live stream. */ 68 public interface PlayerEmsgCallback { 69 70 /** Called when the current manifest should be refreshed. */ onDashManifestRefreshRequested()71 void onDashManifestRefreshRequested(); 72 73 /** 74 * Called when the manifest with the publish time has been expired. 75 * 76 * @param expiredManifestPublishTimeUs The manifest publish time that has been expired. 77 */ onDashManifestPublishTimeExpired(long expiredManifestPublishTimeUs)78 void onDashManifestPublishTimeExpired(long expiredManifestPublishTimeUs); 79 } 80 81 private final Allocator allocator; 82 private final PlayerEmsgCallback playerEmsgCallback; 83 private final EventMessageDecoder decoder; 84 private final Handler handler; 85 private final TreeMap<Long, Long> manifestPublishTimeToExpiryTimeUs; 86 87 private DashManifest manifest; 88 89 private long expiredManifestPublishTimeUs; 90 private long lastLoadedChunkEndTimeUs; 91 private long lastLoadedChunkEndTimeBeforeRefreshUs; 92 private boolean isWaitingForManifestRefresh; 93 private boolean released; 94 95 /** 96 * @param manifest The initial manifest. 97 * @param playerEmsgCallback The callback that this event handler can invoke when handling emsg 98 * messages that generate DASH media source events. 99 * @param allocator An {@link Allocator} from which allocations can be obtained. 100 */ PlayerEmsgHandler( DashManifest manifest, PlayerEmsgCallback playerEmsgCallback, Allocator allocator)101 public PlayerEmsgHandler( 102 DashManifest manifest, PlayerEmsgCallback playerEmsgCallback, Allocator allocator) { 103 this.manifest = manifest; 104 this.playerEmsgCallback = playerEmsgCallback; 105 this.allocator = allocator; 106 107 manifestPublishTimeToExpiryTimeUs = new TreeMap<>(); 108 handler = Util.createHandler(/* callback= */ this); 109 decoder = new EventMessageDecoder(); 110 lastLoadedChunkEndTimeUs = C.TIME_UNSET; 111 lastLoadedChunkEndTimeBeforeRefreshUs = C.TIME_UNSET; 112 } 113 114 /** 115 * Updates the {@link DashManifest} that this handler works on. 116 * 117 * @param newManifest The updated manifest. 118 */ updateManifest(DashManifest newManifest)119 public void updateManifest(DashManifest newManifest) { 120 isWaitingForManifestRefresh = false; 121 expiredManifestPublishTimeUs = C.TIME_UNSET; 122 this.manifest = newManifest; 123 removePreviouslyExpiredManifestPublishTimeValues(); 124 } 125 maybeRefreshManifestBeforeLoadingNextChunk(long presentationPositionUs)126 /* package */ boolean maybeRefreshManifestBeforeLoadingNextChunk(long presentationPositionUs) { 127 if (!manifest.dynamic) { 128 return false; 129 } 130 if (isWaitingForManifestRefresh) { 131 return true; 132 } 133 boolean manifestRefreshNeeded = false; 134 // Find the smallest publishTime (greater than or equal to the current manifest's publish time) 135 // that has a corresponding expiry time. 136 Map.Entry<Long, Long> expiredEntry = ceilingExpiryEntryForPublishTime(manifest.publishTimeMs); 137 if (expiredEntry != null) { 138 long expiredPointUs = expiredEntry.getValue(); 139 if (expiredPointUs < presentationPositionUs) { 140 expiredManifestPublishTimeUs = expiredEntry.getKey(); 141 notifyManifestPublishTimeExpired(); 142 manifestRefreshNeeded = true; 143 } 144 } 145 if (manifestRefreshNeeded) { 146 maybeNotifyDashManifestRefreshNeeded(); 147 } 148 return manifestRefreshNeeded; 149 } 150 151 /** 152 * For live streaming with emsg event stream, forward seeking can seek pass the emsg messages that 153 * signals end-of-stream or Manifest expiry, which results in load error. In this case, we should 154 * notify the Dash media source to refresh its manifest. 155 * 156 * @param chunk The chunk whose load encountered the error. 157 * @return True if manifest refresh has been requested, false otherwise. 158 */ maybeRefreshManifestOnLoadingError(Chunk chunk)159 /* package */ boolean maybeRefreshManifestOnLoadingError(Chunk chunk) { 160 if (!manifest.dynamic) { 161 return false; 162 } 163 if (isWaitingForManifestRefresh) { 164 return true; 165 } 166 boolean isAfterForwardSeek = 167 lastLoadedChunkEndTimeUs != C.TIME_UNSET && lastLoadedChunkEndTimeUs < chunk.startTimeUs; 168 if (isAfterForwardSeek) { 169 // if we are after a forward seek, and the playback is dynamic with embedded emsg stream, 170 // there's a chance that we have seek over the emsg messages, in which case we should ask 171 // media source for a refresh. 172 maybeNotifyDashManifestRefreshNeeded(); 173 return true; 174 } 175 return false; 176 } 177 178 /** 179 * Called when the a new chunk in the current media stream has been loaded. 180 * 181 * @param chunk The chunk whose load has been completed. 182 */ 183 /* package */ void onChunkLoadCompleted(Chunk chunk) { 184 if (lastLoadedChunkEndTimeUs != C.TIME_UNSET || chunk.endTimeUs > lastLoadedChunkEndTimeUs) { 185 lastLoadedChunkEndTimeUs = chunk.endTimeUs; 186 } 187 } 188 189 /** 190 * Returns whether an event with given schemeIdUri and value is a DASH emsg event targeting the 191 * player. 192 */ 193 public static boolean isPlayerEmsgEvent(String schemeIdUri, String value) { 194 return "urn:mpeg:dash:event:2012".equals(schemeIdUri) 195 && ("1".equals(value) || "2".equals(value) || "3".equals(value)); 196 } 197 198 /** Returns a {@link TrackOutput} that emsg messages could be written to. */ 199 public PlayerTrackEmsgHandler newPlayerTrackEmsgHandler() { 200 return new PlayerTrackEmsgHandler(allocator); 201 } 202 203 /** Release this emsg handler. It should not be reused after this call. */ 204 public void release() { 205 released = true; 206 handler.removeCallbacksAndMessages(null); 207 } 208 209 @Override 210 public boolean handleMessage(Message message) { 211 if (released) { 212 return true; 213 } 214 switch (message.what) { 215 case (EMSG_MANIFEST_EXPIRED): 216 ManifestExpiryEventInfo messageObj = (ManifestExpiryEventInfo) message.obj; 217 handleManifestExpiredMessage( 218 messageObj.eventTimeUs, messageObj.manifestPublishTimeMsInEmsg); 219 return true; 220 default: 221 // Do nothing. 222 } 223 return false; 224 } 225 226 // Internal methods. 227 228 private void handleManifestExpiredMessage(long eventTimeUs, long manifestPublishTimeMsInEmsg) { 229 Long previousExpiryTimeUs = manifestPublishTimeToExpiryTimeUs.get(manifestPublishTimeMsInEmsg); 230 if (previousExpiryTimeUs == null) { 231 manifestPublishTimeToExpiryTimeUs.put(manifestPublishTimeMsInEmsg, eventTimeUs); 232 } else { 233 if (previousExpiryTimeUs > eventTimeUs) { 234 manifestPublishTimeToExpiryTimeUs.put(manifestPublishTimeMsInEmsg, eventTimeUs); 235 } 236 } 237 } 238 239 private @Nullable Map.Entry<Long, Long> ceilingExpiryEntryForPublishTime(long publishTimeMs) { 240 return manifestPublishTimeToExpiryTimeUs.ceilingEntry(publishTimeMs); 241 } 242 243 private void removePreviouslyExpiredManifestPublishTimeValues() { 244 for (Iterator<Map.Entry<Long, Long>> it = 245 manifestPublishTimeToExpiryTimeUs.entrySet().iterator(); 246 it.hasNext(); ) { 247 Map.Entry<Long, Long> entry = it.next(); 248 long expiredManifestPublishTime = entry.getKey(); 249 if (expiredManifestPublishTime < manifest.publishTimeMs) { 250 it.remove(); 251 } 252 } 253 } 254 255 private void notifyManifestPublishTimeExpired() { 256 playerEmsgCallback.onDashManifestPublishTimeExpired(expiredManifestPublishTimeUs); 257 } 258 259 /** Requests DASH media manifest to be refreshed if necessary. */ 260 private void maybeNotifyDashManifestRefreshNeeded() { 261 if (lastLoadedChunkEndTimeBeforeRefreshUs != C.TIME_UNSET 262 && lastLoadedChunkEndTimeBeforeRefreshUs == lastLoadedChunkEndTimeUs) { 263 // Already requested manifest refresh. 264 return; 265 } 266 isWaitingForManifestRefresh = true; 267 lastLoadedChunkEndTimeBeforeRefreshUs = lastLoadedChunkEndTimeUs; 268 playerEmsgCallback.onDashManifestRefreshRequested(); 269 } 270 271 private static long getManifestPublishTimeMsInEmsg(EventMessage eventMessage) { 272 try { 273 return parseXsDateTime(Util.fromUtf8Bytes(eventMessage.messageData)); 274 } catch (ParserException ignored) { 275 // if we can't parse this event, ignore 276 return C.TIME_UNSET; 277 } 278 } 279 280 /** Handles emsg messages for a specific track for the player. */ 281 public final class PlayerTrackEmsgHandler implements TrackOutput { 282 283 private final SampleQueue sampleQueue; 284 private final FormatHolder formatHolder; 285 private final MetadataInputBuffer buffer; 286 287 /* package */ PlayerTrackEmsgHandler(Allocator allocator) { 288 this.sampleQueue = 289 new SampleQueue( 290 allocator, 291 /* playbackLooper= */ handler.getLooper(), 292 DrmSessionManager.getDummyDrmSessionManager(), 293 new MediaSourceEventDispatcher()); 294 formatHolder = new FormatHolder(); 295 buffer = new MetadataInputBuffer(); 296 } 297 298 @Override 299 public void format(Format format) { 300 sampleQueue.format(format); 301 } 302 303 @Override 304 public int sampleData( 305 DataReader input, int length, boolean allowEndOfInput, @SampleDataPart int sampleDataPart) 306 throws IOException { 307 return sampleQueue.sampleData(input, length, allowEndOfInput); 308 } 309 310 @Override 311 public void sampleData(ParsableByteArray data, int length, @SampleDataPart int sampleDataPart) { 312 sampleQueue.sampleData(data, length); 313 } 314 315 @Override 316 public void sampleMetadata( 317 long timeUs, int flags, int size, int offset, @Nullable CryptoData encryptionData) { 318 sampleQueue.sampleMetadata(timeUs, flags, size, offset, encryptionData); 319 parseAndDiscardSamples(); 320 } 321 322 /** 323 * For live streaming, check if the DASH manifest is expired before the next segment start time. 324 * If it is, the DASH media source will be notified to refresh the manifest. 325 * 326 * @param presentationPositionUs The next load position in presentation time. 327 * @return True if manifest refresh has been requested, false otherwise. 328 */ 329 public boolean maybeRefreshManifestBeforeLoadingNextChunk(long presentationPositionUs) { 330 return PlayerEmsgHandler.this.maybeRefreshManifestBeforeLoadingNextChunk( 331 presentationPositionUs); 332 } 333 334 /** 335 * Called when the a new chunk in the current media stream has been loaded. 336 * 337 * @param chunk The chunk whose load has been completed. 338 */ 339 public void onChunkLoadCompleted(Chunk chunk) { 340 PlayerEmsgHandler.this.onChunkLoadCompleted(chunk); 341 } 342 343 /** 344 * For live streaming with emsg event stream, forward seeking can seek pass the emsg messages 345 * that signals end-of-stream or Manifest expiry, which results in load error. In this case, we 346 * should notify the Dash media source to refresh its manifest. 347 * 348 * @param chunk The chunk whose load encountered the error. 349 * @return True if manifest refresh has been requested, false otherwise. 350 */ 351 public boolean maybeRefreshManifestOnLoadingError(Chunk chunk) { 352 return PlayerEmsgHandler.this.maybeRefreshManifestOnLoadingError(chunk); 353 } 354 355 /** Release this track emsg handler. It should not be reused after this call. */ 356 public void release() { 357 sampleQueue.release(); 358 } 359 360 // Internal methods. 361 362 private void parseAndDiscardSamples() { 363 while (sampleQueue.isReady(/* loadingFinished= */ false)) { 364 MetadataInputBuffer inputBuffer = dequeueSample(); 365 if (inputBuffer == null) { 366 continue; 367 } 368 long eventTimeUs = inputBuffer.timeUs; 369 Metadata metadata = decoder.decode(inputBuffer); 370 EventMessage eventMessage = (EventMessage) metadata.get(0); 371 if (isPlayerEmsgEvent(eventMessage.schemeIdUri, eventMessage.value)) { 372 parsePlayerEmsgEvent(eventTimeUs, eventMessage); 373 } 374 } 375 sampleQueue.discardToRead(); 376 } 377 378 @Nullable 379 private MetadataInputBuffer dequeueSample() { 380 buffer.clear(); 381 int result = 382 sampleQueue.read( 383 formatHolder, 384 buffer, 385 /* formatRequired= */ false, 386 /* loadingFinished= */ false, 387 /* decodeOnlyUntilUs= */ 0); 388 if (result == C.RESULT_BUFFER_READ) { 389 buffer.flip(); 390 return buffer; 391 } 392 return null; 393 } 394 395 private void parsePlayerEmsgEvent(long eventTimeUs, EventMessage eventMessage) { 396 long manifestPublishTimeMsInEmsg = getManifestPublishTimeMsInEmsg(eventMessage); 397 if (manifestPublishTimeMsInEmsg == C.TIME_UNSET) { 398 return; 399 } 400 onManifestExpiredMessageEncountered(eventTimeUs, manifestPublishTimeMsInEmsg); 401 } 402 403 private void onManifestExpiredMessageEncountered( 404 long eventTimeUs, long manifestPublishTimeMsInEmsg) { 405 ManifestExpiryEventInfo manifestExpiryEventInfo = 406 new ManifestExpiryEventInfo(eventTimeUs, manifestPublishTimeMsInEmsg); 407 handler.sendMessage(handler.obtainMessage(EMSG_MANIFEST_EXPIRED, manifestExpiryEventInfo)); 408 } 409 } 410 411 /** Holds information related to a manifest expiry event. */ 412 private static final class ManifestExpiryEventInfo { 413 414 public final long eventTimeUs; 415 public final long manifestPublishTimeMsInEmsg; 416 417 public ManifestExpiryEventInfo(long eventTimeUs, long manifestPublishTimeMsInEmsg) { 418 this.eventTimeUs = eventTimeUs; 419 this.manifestPublishTimeMsInEmsg = manifestPublishTimeMsInEmsg; 420 } 421 } 422 } 423