• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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