Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support drm init data per discontinuity #4180

Open
wants to merge 4 commits into
base: dev-v2
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,7 @@ public void getNextChunk(HlsMediaChunk previous, long playbackPositionUs, long l
isTimestampMaster,
timestampAdjuster,
previous,
mediaPlaylist.drmInitData,
segment.drmInitData,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is an issue with this: If the first segment is not encrypted, then the media chunk will expose no DRM init data, and the track selector will think the segment is not encrypted. I'll try to think about a solution for this.

Copy link
Contributor Author

@TakuSemba TakuSemba May 22, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I coundn't understand your concern.
I know the fact that shaka-packager creates a playlist like ↓ for a Widevine-encrypted content.
In that case, I believe that video_1.m4s is not (and should not be) encrypted and video_2.m4s ~ are encrypted with Widevine.
I think the track selector should think the segment is not encrypted, because it's not encrypted. 🤔

#EXTM3U
#EXT-X-VERSION:6
#EXT-X-TARGETDURATION:18
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-MAP:URI="video_init.mp4"
#EXTINF:13.583,
video_1.m4s
#EXT-X-DISCONTINUITY
#EXT-X-KEY:METHOD=SAMPLE-AES-CTR,URI="data:text/…
#EXTINF:10.417,
video_2.m4s
#EXTINF:7.417,
video_3.m4s
…

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The track selector does not know about specific segments, but rather about tracks. The issue is that the track is indeed encrypted (even if some of the segments are clear), but the first segment's format (unencrypted), as things are now, will be propagated as the track's format.

Copy link
Contributor Author

@TakuSemba TakuSemba May 22, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the replay! I understood what you mean.

will be propagated as the track's format.

but, I could not find the exact place where the track selector thinks the segment is not encrypted or a wrong track's format is propagated.

you want to me to fix it on this PR?
I'm sorry to bother you, but it would be really appreciated if you could tell me the specific point where it's happening.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I suspect might be a solution for this is keeping a DrmInitData in the media playlist which contains SchemeData's with null SchemeData#data, so as to signal that encryption of a specific CDM is eventually there, without tying it to a specific key/pssh. This DrmInitData would then be propagated by the period for the track selector to consume. This field would be populated by the playlist parser after parsing all EXT-X-KEYS present in the playlist.

Copy link
Contributor Author

@TakuSemba TakuSemba May 24, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure about what you suggest.
Does your suggestion mean HlsMediaPlaylist should hold a single DrmInitData with null SchemeData#data? (Instead of letting each segment hold a DrmInitData)

and again, Im playing the content, which has the first non-encrypted segment and the other widevine-encrypted segments, and It plays without any problems, and could not find the place where wrong track's formats are created or propagated while breakpointing inside exoplayer.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does your suggestion mean HlsMediaPlaylist should hold a single DrmInitData with null SchemeData#data

No, it should contain as many scheme datas as encryption schemes there are in the content. For example, if the media is encrypted using both Widevine and Playready, in would contain two SchemeDatas, one for each.

Im playing the content, which has the first non-encrypted segment and the other widevine-encrypted segments

This issue does not affect playback. We have a similar problem to the one introduced here in chunkless preparation (but unlike here, we cannot do much without downloading extra stuff, which defeats the purpose of chunkless preparation).

The issue is track selection. The tracks exposed by the MediaPeriod will not contain DRM data, so the track selector will assume the content is unencrypted. The resulting behavior is a crash instead of graceful handling of unsupported DRM schemes. In this case, you are playing on a device that supports the used DRM scheme. If it didn't, the player would crash instead (potentially -though rare- with a native crash). Hopefully this is clear.

Copy link
Contributor Author

@TakuSemba TakuSemba May 27, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the explanation!.

The tracks exposed by the MediaPeriod will not contain DRM data

I think the place where the MediaPeriod exposes the tracks is here.

But, it seems like the MediaPeriod has the track groups with non-null drmInitData in it.

2018-05-24 19 42 26

http://160.16.54.236:8081/drmtest/media/drm/180p/master.m3u8 is the stream that I tested .

{
  "name": "HLS Plain (180p)",
  "uri": "http://160.16.54.236:8081/drmtest/media/plain/180p/master.m3u8"
},
{
  "name": "HLS DRM (180p)",
  "uri": "http://160.16.54.236:8081/drmtest/media/drm/180p/master.m3u8",
  "drm_scheme": "widevine",
  "drm_license_url": "https://proxy.uat.widevine.com/proxy"
},
{
  "name": "HLS Plain (180p) -> DRM (180p)",
  "uri": "http://160.16.54.236:8081/drmtest/playlist/plain180p-drm180p/master.m3u8",
  "drm_scheme": "widevine",
  "drm_license_url": "https://proxy.uat.widevine.com/proxy"
},

And your solution is not clear to me yet.

keeping a DrmInitData in the media playlist which contains SchemeData's with null SchemeData#data

does your suggestion mean keeping an another DrmInitData only for the first segment like ↓?

  private static HlsMediaPlaylist parseMediaPlaylist(LineIterator iterator, String baseUri)
      throws IOException {
    ...
    String encryptionKeyUri = null;
    String encryptionIV = null;
    DrmInitData drmInitData = null;
    DrmInitData drmInitDataforFirstSegment = // drmInitData which contains SchemeData's with null SchemeData#data;

    String line;
    while (iterator.hasNext()) {
      line = iterator.next();
        ...
    }
  }

encryptionKey,
encryptionIv);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ public static final class Segment implements Comparable<Long> {
* used for all segments that share an EXT-X-MAP tag.
*/
@Nullable public final Segment initializationSegment;
/**
* {@link DrmInitData} for sample decryption, or null if the segment's samples are not
* DRM-protected.
*/
@Nullable public final DrmInitData drmInitData;
/** The duration of the segment in microseconds, as defined by #EXTINF. */
public final long durationUs;
/**
Expand Down Expand Up @@ -81,7 +86,8 @@ public static final class Segment implements Comparable<Long> {
* @param byterangeLength See {@link #byterangeLength}.
*/
public Segment(String uri, long byterangeOffset, long byterangeLength) {
this(uri, null, 0, -1, C.TIME_UNSET, null, null, byterangeOffset, byterangeLength, false);
this(uri, null, null, 0, -1, C.TIME_UNSET, null, null, byterangeOffset, byterangeLength,
false);
}

/**
Expand All @@ -99,6 +105,7 @@ public Segment(String uri, long byterangeOffset, long byterangeLength) {
public Segment(
String url,
Segment initializationSegment,
@Nullable DrmInitData drmInitData,
long durationUs,
int relativeDiscontinuitySequence,
long relativeStartTimeUs,
Expand All @@ -109,6 +116,7 @@ public Segment(
boolean hasGapTag) {
this.url = url;
this.initializationSegment = initializationSegment;
this.drmInitData = drmInitData;
this.durationUs = durationUs;
this.relativeDiscontinuitySequence = relativeDiscontinuitySequence;
this.relativeStartTimeUs = relativeStartTimeUs;
Expand Down Expand Up @@ -183,11 +191,6 @@ public int compareTo(@NonNull Long relativeStartTimeUs) {
* Whether the playlist contains a #EXT-X-PROGRAM-DATE-TIME tag.
*/
public final boolean hasProgramDateTime;
/**
* DRM initialization data for sample decryption, or null if none of the segment uses sample
* encryption.
*/
public final DrmInitData drmInitData;
/**
* The list of segments in the playlist.
*/
Expand All @@ -211,7 +214,6 @@ public int compareTo(@NonNull Long relativeStartTimeUs) {
* @param hasIndependentSegmentsTag See {@link #hasIndependentSegmentsTag}.
* @param hasEndTag See {@link #hasEndTag}.
* @param hasProgramDateTime See {@link #hasProgramDateTime}.
* @param drmInitData See {@link #drmInitData}.
* @param segments See {@link #segments}.
*/
public HlsMediaPlaylist(
Expand All @@ -228,7 +230,6 @@ public HlsMediaPlaylist(
boolean hasIndependentSegmentsTag,
boolean hasEndTag,
boolean hasProgramDateTime,
DrmInitData drmInitData,
List<Segment> segments) {
super(baseUri, tags);
this.playlistType = playlistType;
Expand All @@ -241,7 +242,6 @@ public HlsMediaPlaylist(
this.hasIndependentSegmentsTag = hasIndependentSegmentsTag;
this.hasEndTag = hasEndTag;
this.hasProgramDateTime = hasProgramDateTime;
this.drmInitData = drmInitData;
this.segments = Collections.unmodifiableList(segments);
if (!segments.isEmpty()) {
Segment last = segments.get(segments.size() - 1);
Expand Down Expand Up @@ -309,7 +309,6 @@ public HlsMediaPlaylist copyWith(long startTimeUs, int discontinuitySequence) {
hasIndependentSegmentsTag,
hasEndTag,
hasProgramDateTime,
drmInitData,
segments);
}

Expand Down Expand Up @@ -337,7 +336,6 @@ public HlsMediaPlaylist copyWithEndTag() {
hasIndependentSegmentsTag,
/* hasEndTag= */ true,
hasProgramDateTime,
drmInitData,
segments);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,8 @@ private static HlsMediaPlaylist parseMediaPlaylist(LineIterator iterator, String
schemeData);
}
}
} else {
drmInitData = null;
}
} else if (line.startsWith(TAG_BYTERANGE)) {
String byteRange = parseStringAttr(line, REGEX_BYTERANGE);
Expand Down Expand Up @@ -475,6 +477,7 @@ private static HlsMediaPlaylist parseMediaPlaylist(LineIterator iterator, String
new Segment(
line,
initializationSegment,
drmInitData,
segmentDurationUs,
relativeDiscontinuitySequence,
segmentStartTimeUs,
Expand Down Expand Up @@ -506,7 +509,6 @@ private static HlsMediaPlaylist parseMediaPlaylist(LineIterator iterator, String
hasIndependentSegmentsTag,
hasEndTag,
/* hasProgramDateTime= */ playlistStartTimeUs != 0,
drmInitData,
segments);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,8 +160,9 @@ public void testParseSampleAesMethod() throws Exception {
new ByteArrayInputStream(playlistString.getBytes(Charset.forName(C.UTF8_NAME)));
HlsMediaPlaylist playlist =
(HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);
assertThat(playlist.drmInitData.schemeType).isEqualTo(C.CENC_TYPE_cbcs);
assertThat(playlist.drmInitData.get(0).matches(C.WIDEVINE_UUID)).isTrue();
assertThat(playlist.segments.get(0).drmInitData).isNull();
assertThat(playlist.segments.get(1).drmInitData.schemeType).isEqualTo(C.CENC_TYPE_cbcs);
assertThat(playlist.segments.get(1).drmInitData.get(0).matches(C.WIDEVINE_UUID)).isTrue();
}

@Test
Expand All @@ -184,8 +185,9 @@ public void testParseSampleAesCencMethod() throws Exception {
new ByteArrayInputStream(playlistString.getBytes(Charset.forName(C.UTF8_NAME)));
HlsMediaPlaylist playlist =
(HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);
assertThat(playlist.drmInitData.schemeType).isEqualTo(C.CENC_TYPE_cenc);
assertThat(playlist.drmInitData.get(0).matches(C.WIDEVINE_UUID)).isTrue();
assertThat(playlist.segments.get(0).drmInitData).isNull();
assertThat(playlist.segments.get(1).drmInitData.schemeType).isEqualTo(C.CENC_TYPE_cenc);
assertThat(playlist.segments.get(1).drmInitData.get(0).matches(C.WIDEVINE_UUID)).isTrue();
}

@Test
Expand All @@ -208,8 +210,46 @@ public void testParseSampleAesCtrMethod() throws Exception {
new ByteArrayInputStream(playlistString.getBytes(Charset.forName(C.UTF8_NAME)));
HlsMediaPlaylist playlist =
(HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);
assertThat(playlist.drmInitData.schemeType).isEqualTo(C.CENC_TYPE_cenc);
assertThat(playlist.drmInitData.get(0).matches(C.WIDEVINE_UUID)).isTrue();
assertThat(playlist.segments.get(0).drmInitData).isNull();
assertThat(playlist.segments.get(1).drmInitData.schemeType).isEqualTo(C.CENC_TYPE_cenc);
assertThat(playlist.segments.get(1).drmInitData.get(0).matches(C.WIDEVINE_UUID)).isTrue();
}

@Test
public void testParseMultipleEncryptionsMethod() throws Exception {
Uri playlistUri = Uri.parse("https://example.com/test.m3u8");
String playlistString =
"#EXTM3U\n"
+ "#EXT-X-MEDIA-SEQUENCE:0\n"
+ "#EXTINF:8,\n"
+ "https://priv.example.com/1.ts\n"
+ "\n"
+ "#EXT-X-KEY:URI=\"data:text/plain;base64,VGhpcyBpcyBhbiBlYXN0ZXIgZWdn\","
+ "IV=0x9358382AEB449EE23C3D809DA0B9CCD3,KEYFORMATVERSIONS=\"1\","
+ "KEYFORMAT=\"urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed\","
+ "IV=0x1566B,METHOD=SAMPLE-AES-CENC \n"
+ "#EXTINF:8,\n"
+ "https://priv.example.com/2.ts\n"
+ "#EXT-X-KEY:METHOD=NONE\n"
+ "#EXTINF:8,\n"
+ "https://priv.example.com/3.ts\n"
+ "#EXT-X-KEY:URI=\"data:text/plain;base64,VGhpcyBpcyBhbiBlYXN0ZXIgZWdn\","
+ "IV=0x9358382AEB449EE23C3D809DA0B9CCD3,KEYFORMATVERSIONS=\"1\","
+ "KEYFORMAT=\"urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed\","
+ "IV=0x1566B,METHOD=SAMPLE-AES-CENC \n"
+ "#EXTINF:8,\n"
+ "https://priv.example.com/4.ts\n"
+ "#EXT-X-ENDLIST\n";
InputStream inputStream =
new ByteArrayInputStream(playlistString.getBytes(Charset.forName(C.UTF8_NAME)));
HlsMediaPlaylist playlist =
(HlsMediaPlaylist) new HlsPlaylistParser().parse(playlistUri, inputStream);
assertThat(playlist.segments.get(0).drmInitData).isNull();
assertThat(playlist.segments.get(1).drmInitData.schemeType).isEqualTo(C.CENC_TYPE_cenc);
assertThat(playlist.segments.get(1).drmInitData.get(0).matches(C.WIDEVINE_UUID)).isTrue();
assertThat(playlist.segments.get(2).drmInitData).isNull();
assertThat(playlist.segments.get(3).drmInitData.schemeType).isEqualTo(C.CENC_TYPE_cenc);
assertThat(playlist.segments.get(3).drmInitData.get(0).matches(C.WIDEVINE_UUID)).isTrue();
}

@Test
Expand Down