Are you also annoyed when you want to download some video seminar but they decide to hide the download URL, or even worse encrypt the video stream?
Well, this happened to me while trying to archive the ToonBoom panel with Mercury Filmworks and this is how to get around BigMarker.com “content protection”.
First thing first: let’s open up the page.
You can register or login if you want but it’s not necessary. Let’s fire up the document inspector and let’s look up for knows video streaming strings like .webm
, .mp4
, .m3u8
, .mpd
etc.
$(function() {
var watched = false;
$('#bm-video-preview-cover, #bm-video-play-button').click(function() {
if ($('#register-to-view-recording-box').length == 0) {
$('#bm-video-preview-cover').remove();
$('#bm-video-thumbnail').hide();
if(!bmVideoPlayer.videoLoaded){
bmVideoPlayer.loadVideo({
mp4Url: "https://thatlongfirsturl/video.mp4",
trackList: trackList,
adaptive_streaming: {"dash_manifest_url":"https://thatlongfirsturl/dash/dash.mpd","dash_encryption_key":"90dffd13b6c2b0ce029b435f3c78c7ab","dash_encryption_kid":"c26f9f57c2fcab040df214da5ecbcccc","hls_manifest_url":"https://thatlongfirsturl/hls/hls.m3u8"},
playerPanorama: false,
onloadedmetadata: function(video){
video.play();
},
ontimeupdate: function(video){
if(bmVideoPlayer.watch_duration < video.currentTime) bmVideoPlayer.watch_duration = video.currentTime;
}
});
}
} else {
}
if (!watched) {
watched = true;
$.ajax({
url: "/conferences/9aaf6238312d/recording_watched"
});
}
});
We end up with 3 video sources: a simple .mp4
file and 2 video “playlists”.
The mp4 file is straight to download and to play. It is a 286Mb file in 1080p and with the audio track included. Not bad for a simple search.
But what about the other two playlists? They are used for adaptive streaming (DASH) and the other parameters are mentioning some sort of encryption.
Let’s pull thses files and let’s see what’s the deal with them.
ffmpeg -i "https://thatlongfirsturl/dash/dash.mpd" -o video.mp4
Mhhh, the output is a bit verbose and it gives us an error in the end. Let’s open up the .mpd file and check it.
<?xml version="1.0" encoding="UTF-8"?>
<!--Generated with https://github.com/google/shaka-packager version faa9a3e-release-->
<MPD xmlns="urn:mpeg:dash:schema:mpd:2011" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:mpeg:dash:schema:mpd:2011 DASH-MPD.xsd" xmlns:cenc="urn:mpeg:cenc:2013" profiles="urn:mpeg:dash:profile:isoff-on-demand:2011" minBufferTime="PT2S" type="static" mediaPresentationDuration="PT4037.63330078125S">
<Period id="0">
<AdaptationSet id="0" contentType="video" maxWidth="1920" maxHeight="1080" frameRate="15360/512" subsegmentAlignment="true" par="16:9">
<ContentProtection value="cenc" schemeIdUri="urn:mpeg:dash:mp4protection:2011" cenc:default_KID="c26f9f57-c2fc-ab04-0df2-14da5ecbcccc"/>
<ContentProtection schemeIdUri="urn:uuid:1077efec-c0b2-4d02-ace3-3c1e52e2fb4b">
<cenc:pssh>AAAANHBzc2gBAAAAEHfv7MCyTQKs4zweUuL7SwAAAAHCb59XwvyrBA3yFNpey8zMAAAAAA==</cenc:pssh>
</ContentProtection>
<!--other sizes that I deleted to keep things simple-->
<Representation id="6" bandwidth="1165920" codecs="avc1.64001f" mimeType="video/mp4" sar="1:1" width="1280" height="720">
<BaseURL>1280.mp4</BaseURL>
<SegmentBase indexRange="1133-9240" timescale="15360">
<Initialization range="0-1132"/>
</SegmentBase>
</Representation>
<Representation id="7" bandwidth="2008168" codecs="avc1.640028" mimeType="video/mp4" sar="1:1" width="1920" height="1080">
<BaseURL>1920.mp4</BaseURL>
<SegmentBase indexRange="1135-9242" timescale="15360">
<Initialization range="0-1134"/>
</SegmentBase>
</Representation>
</AdaptationSet>
<AdaptationSet id="1" contentType="audio" lang="en" subsegmentAlignment="true">
<ContentProtection value="cenc" schemeIdUri="urn:mpeg:dash:mp4protection:2011" cenc:default_KID="c26f9f57-c2fc-ab04-0df2-14da5ecbcccc"/>
<ContentProtection schemeIdUri="urn:uuid:1077efec-c0b2-4d02-ace3-3c1e52e2fb4b">
<cenc:pssh>AAAANHBzc2gBAAAAEHfv7MCyTQKs4zweUuL7SwAAAAHCb59XwvyrBA3yFNpey8zMAAAAAA==</cenc:pssh>
</ContentProtection>
<Representation id="2" bandwidth="143504" codecs="mp4a.40.2" mimeType="audio/mp4" audioSamplingRate="48000">
<AudioChannelConfiguration schemeIdUri="urn:mpeg:dash:23003:3:audio_channel_configuration:2011" value="2"/>
<BaseURL>audio.mp4</BaseURL>
<SegmentBase indexRange="1025-9132" timescale="48000">
<Initialization range="0-1024"/>
</SegmentBase>
</Representation>
</AdaptationSet>
</Period>
</MPD>
Cool, we have another simple mp4 file that we can pull with
curl "https://thatlongfirsturl/dash/1920.mp4" -o 1920.mp4
Ok, now we have a 347Mb file. Maybe we have found a less compressed source. Let’s see if we can notice the difference… aaand the video blacks out after the intro. Great.
Not a big surprise if we noticed these lines in the mpd file.
<ContentProtection value="cenc" schemeIdUri="urn:mpeg:dash:mp4protection:2011" cenc:default_KID="c26f9f57-c2fc-ab04-0df2-14da5ecbcccc"/>
<ContentProtection schemeIdUri="urn:uuid:1077efec-c0b2-4d02-ace3-3c1e52e2fb4b">
<cenc:pssh>AAAANHBzc2gBAAAAEHfv7MCyTQKs4zweUuL7SwAAAAHCb59XwvyrBA3yFNpey8zMAAAAAA==</cenc:pssh>
</ContentProtection>
So, we have a (hopefully) bigger quality video file. But how to decrypt it???
Well, shaka
is a known Google product for video distribuition, maybe they use Widevine but it doesn’t look like it.
Let’s look at our video player initialization:
bmVideoPlayer.loadVideo({
mp4Url: "https://thatlongfirsturl/video.mp4",
trackList: trackList,
adaptive_streaming: {
"dash_manifest_url":"https://thatlongfirsturl/dash/dash.mpd",
"dash_encryption_key":"90dffd13b6c2b0ce029b435f3c78c7ab",
"dash_encryption_kid":"c26f9f57c2fcab040df214da5ecbcccc",
"hls_manifest_url":"https://thatlongfirsturl/hls/hls.m3u8"
},
playerPanorama: false,
onloadedmetadata: function(video){
video.play();
},
ontimeupdate: function(video){
if(bmVideoPlayer.watch_duration < video.currentTime) bmVideoPlayer.watch_duration = video.currentTime;
}
});
Do you see them? Yeah, those look a lot like decryption keys. Usually there is a whole negotiation process involving hardware calls with your graphic card asking your LCD monitor to encrypt the video signal but we can just try to use those keys and see what happens.
We are going to use mp4decrypt from the Bento4 suite because FFMPEG is always a bit picky when dealing with encypted content.
mp4decrypt --key c26f9f57c2fcab040df214da5ecbcccc:90dffd13b6c2b0ce029b435f3c78c7ab 1920.mp4 decrypted.mp4
Aaaand it works… Ok, we have no audio but that one is pretty trivial at this point.
I encountered some problems while trying to decrypt the hls playlist, it seems like the server returns a broken key for decryption.
So, did we gain any quality with this whole endeavour??
I took two screenshots at a random timestamp, can you spot the difference?
ffmpeg -ss 00:43:45 -i file.mp4 -frames:v 1 -q:v 2 output.png
Ok, maybe it’s hard to spot at this resolution (you can always click for the full size btw). Let’s compare on the parts where video compression struggles: text and moving objects.
Video encoders can usually make text hard to read because of compression. In our case it seems like they are both readable with no discernible differences, having static text surely helps.
Let’s move to the moving objects, in our case the heads of the speakers.
Mmmmm, IMHO the smaller file has actually a better quality in this case, we can see more details in the beard and in the eyes, there is also less banding on the background.
(Keep in mind that this is really subjective so you may prefer the other source and that’s ok.)
How can this happen? My hypothesis is that the mpd (and maybe the hls file too) were generated on the fly with the feed ingestion, while the mp4 file was generated later for archiving and private downloading with more lenient time constraints (Remenber: in video encoding you can only choose two out of three: video quality, encoding time or low bandwith).
The unencrypted low-hanging fruit was the right choice since the start, but it was fun to dig through this.
Thanks for following through in this weird journey.