Shaka Packager SDK
simple_hls_notifier.cc
1 // Copyright 2016 Google Inc. All rights reserved.
2 //
3 // Use of this source code is governed by a BSD-style
4 // license that can be found in the LICENSE file or at
5 // https://developers.google.com/open-source/licenses/bsd
6 
7 #include "packager/hls/base/simple_hls_notifier.h"
8 
9 #include <gflags/gflags.h>
10 #include <cmath>
11 
12 #include "packager/base/base64.h"
13 #include "packager/base/files/file_path.h"
14 #include "packager/base/logging.h"
15 #include "packager/base/optional.h"
16 #include "packager/base/strings/string_number_conversions.h"
17 #include "packager/base/strings/stringprintf.h"
18 #include "packager/hls/base/media_playlist.h"
19 #include "packager/media/base/protection_system_ids.h"
20 #include "packager/media/base/protection_system_specific_info.h"
21 #include "packager/media/base/proto_json_util.h"
22 #include "packager/media/base/widevine_pssh_data.pb.h"
23 
24 DEFINE_bool(enable_legacy_widevine_hls_signaling,
25  false,
26  "Specifies whether Legacy Widevine HLS, i.e. v1 is signalled in "
27  "the media playlist. Applies to Widevine protection system in HLS "
28  "with SAMPLE-AES only.");
29 
30 namespace shaka {
31 
32 using base::FilePath;
33 
34 namespace hls {
35 
36 namespace {
37 
38 const char kUriBase64Prefix[] = "data:text/plain;base64,";
39 const char kUriFairPlayPrefix[] = "skd://";
40 const char kWidevineDashIfIopUUID[] =
41  "urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed";
42 
43 bool IsWidevineSystemId(const std::vector<uint8_t>& system_id) {
44  return system_id.size() == arraysize(media::kWidevineSystemId) &&
45  std::equal(system_id.begin(), system_id.end(),
46  media::kWidevineSystemId);
47 }
48 
49 bool IsCommonSystemId(const std::vector<uint8_t>& system_id) {
50  return system_id.size() == arraysize(media::kCommonSystemId) &&
51  std::equal(system_id.begin(), system_id.end(), media::kCommonSystemId);
52 }
53 
54 bool IsFairPlaySystemId(const std::vector<uint8_t>& system_id) {
55  return system_id.size() == arraysize(media::kFairPlaySystemId) &&
56  std::equal(system_id.begin(), system_id.end(),
57  media::kFairPlaySystemId);
58 }
59 
60 std::string Base64EncodeData(const std::string& prefix,
61  const std::string& data) {
62  std::string data_base64;
63  base::Base64Encode(data, &data_base64);
64  return prefix + data_base64;
65 }
66 
67 std::string VectorToString(const std::vector<uint8_t>& v) {
68  return std::string(v.begin(), v.end());
69 }
70 
71 // TODO(rkuroiwa): Dedup these with the functions in MpdBuilder.
72 // If |media_path| is contained in |parent_path|, then
73 // Strips the common path and keep only the relative part of |media_path|.
74 // e.g. if |parent_path| is /some/parent/ and
75 // |media_path| is /some/parent/abc/child/item.ext,
76 // abc/child/item.ext is returned.
77 // else
78 // Returns |media_path|.
79 // The path separator of the output is also changed to "/" if it is not.
80 std::string MakePathRelative(const std::string& media_path,
81  const FilePath& parent_path) {
82  FilePath relative_path;
83  const FilePath child_path = FilePath::FromUTF8Unsafe(media_path);
84  const bool is_child =
85  parent_path.AppendRelativePath(child_path, &relative_path);
86  if (!is_child)
87  relative_path = child_path;
88  return relative_path.NormalizePathSeparatorsTo('/').AsUTF8Unsafe();
89 }
90 
91 // Segment URL is relative to either output directory or the directory
92 // containing the media playlist depends on whether base_url is set.
93 std::string GenerateSegmentUrl(const std::string& segment_name,
94  const std::string& base_url,
95  const std::string& output_dir,
96  const std::string& playlist_file_name) {
97  FilePath output_path = FilePath::FromUTF8Unsafe(output_dir);
98  if (!base_url.empty()) {
99  // Media segment URL is base_url + segment path relative to output
100  // directory.
101  return base_url + MakePathRelative(segment_name, output_path);
102  }
103  // Media segment URL is segment path relative to the directory containing the
104  // playlist.
105  const FilePath playlist_dir =
106  output_path.Append(FilePath::FromUTF8Unsafe(playlist_file_name))
107  .DirName()
108  .AsEndingWithSeparator();
109  return MakePathRelative(segment_name, playlist_dir);
110 }
111 
112 MediaInfo MakeMediaInfoPathsRelativeToPlaylist(
113  const MediaInfo& media_info,
114  const std::string& base_url,
115  const std::string& output_dir,
116  const std::string& playlist_name) {
117  MediaInfo media_info_copy = media_info;
118  if (media_info_copy.has_init_segment_name()) {
119  media_info_copy.set_init_segment_url(
120  GenerateSegmentUrl(media_info_copy.init_segment_name(), base_url,
121  output_dir, playlist_name));
122  }
123  if (media_info_copy.has_media_file_name()) {
124  media_info_copy.set_media_file_url(
125  GenerateSegmentUrl(media_info_copy.media_file_name(), base_url,
126  output_dir, playlist_name));
127  }
128  if (media_info_copy.has_segment_template()) {
129  media_info_copy.set_segment_template_url(
130  GenerateSegmentUrl(media_info_copy.segment_template(), base_url,
131  output_dir, playlist_name));
132  }
133  return media_info_copy;
134 }
135 
136 bool WidevinePsshToJson(const std::vector<uint8_t>& pssh_box,
137  const std::vector<uint8_t>& key_id,
138  std::string* pssh_json) {
139  std::unique_ptr<media::PsshBoxBuilder> pssh_builder =
140  media::PsshBoxBuilder::ParseFromBox(pssh_box.data(), pssh_box.size());
141  if (!pssh_builder) {
142  LOG(ERROR) << "Failed to parse PSSH box.";
143  return false;
144  }
145 
146  media::WidevinePsshData pssh_proto;
147  if (!pssh_proto.ParseFromArray(pssh_builder->pssh_data().data(),
148  pssh_builder->pssh_data().size())) {
149  LOG(ERROR) << "Failed to parse protection_system_specific_data.";
150  return false;
151  }
152 
153  media::WidevineHeader widevine_header;
154 
155  if (pssh_proto.has_provider()) {
156  widevine_header.set_provider(pssh_proto.provider());
157  } else {
158  LOG(WARNING) << "Missing provider in Widevine PSSH. The content may not "
159  "play in some devices.";
160  }
161 
162  if (pssh_proto.has_content_id()) {
163  widevine_header.set_content_id(pssh_proto.content_id());
164  } else {
165  LOG(WARNING) << "Missing content_id in Widevine PSSH. The content may not "
166  "play in some devices.";
167  }
168 
169  // Place the current |key_id| to the front and converts all key_id to hex
170  // format.
171  widevine_header.add_key_ids(base::HexEncode(key_id.data(), key_id.size()));
172  for (const std::string& key_id_in_pssh : pssh_proto.key_id()) {
173  const std::string key_id_hex =
174  base::HexEncode(key_id_in_pssh.data(), key_id_in_pssh.size());
175  if (widevine_header.key_ids(0) != key_id_hex)
176  widevine_header.add_key_ids(key_id_hex);
177  }
178 
179  *pssh_json = media::MessageToJsonString(widevine_header);
180  return true;
181 }
182 
183 base::Optional<MediaPlaylist::EncryptionMethod> StringToEncryptionMethod(
184  const std::string& method) {
185  if (method == "cenc") {
186  return MediaPlaylist::EncryptionMethod::kSampleAesCenc;
187  }
188  if (method == "cbcs") {
189  return MediaPlaylist::EncryptionMethod::kSampleAes;
190  }
191  if (method == "cbca") {
192  // cbca is a place holder for sample aes.
193  return MediaPlaylist::EncryptionMethod::kSampleAes;
194  }
195  return base::nullopt;
196 }
197 
198 void NotifyEncryptionToMediaPlaylist(
199  MediaPlaylist::EncryptionMethod encryption_method,
200  const std::string& uri,
201  const std::vector<uint8_t>& key_id,
202  const std::vector<uint8_t>& iv,
203  const std::string& key_format,
204  const std::string& key_format_version,
205  MediaPlaylist* media_playlist) {
206  std::string iv_string;
207  if (!iv.empty()) {
208  iv_string = "0x" + base::HexEncode(iv.data(), iv.size());
209  }
210  std::string key_id_string;
211  if (!key_id.empty()) {
212  key_id_string = "0x" + base::HexEncode(key_id.data(), key_id.size());
213  }
214 
215  media_playlist->AddEncryptionInfo(
216  encryption_method,
217  uri, key_id_string, iv_string,
218  key_format, key_format_version);
219 }
220 
221 // Creates JSON format and the format similar to MPD.
222 bool HandleWidevineKeyFormats(
223  MediaPlaylist::EncryptionMethod encryption_method,
224  const std::vector<uint8_t>& key_id,
225  const std::vector<uint8_t>& iv,
226  const std::vector<uint8_t>& protection_system_specific_data,
227  MediaPlaylist* media_playlist) {
228  if (FLAGS_enable_legacy_widevine_hls_signaling &&
229  encryption_method == MediaPlaylist::EncryptionMethod::kSampleAes) {
230  // This format allows SAMPLE-AES only.
231  std::string key_uri_data;
232  if (!WidevinePsshToJson(protection_system_specific_data, key_id,
233  &key_uri_data)) {
234  return false;
235  }
236  std::string key_uri_data_base64 =
237  Base64EncodeData(kUriBase64Prefix, key_uri_data);
238  NotifyEncryptionToMediaPlaylist(encryption_method, key_uri_data_base64,
239  std::vector<uint8_t>(), iv, "com.widevine",
240  "1", media_playlist);
241  }
242 
243  std::string pssh_as_string(
244  reinterpret_cast<const char*>(protection_system_specific_data.data()),
245  protection_system_specific_data.size());
246  std::string key_uri_data_base64 =
247  Base64EncodeData(kUriBase64Prefix, pssh_as_string);
248  NotifyEncryptionToMediaPlaylist(encryption_method, key_uri_data_base64,
249  key_id, iv, kWidevineDashIfIopUUID, "1",
250  media_playlist);
251  return true;
252 }
253 
254 bool WriteMediaPlaylist(const std::string& output_dir,
255  MediaPlaylist* playlist) {
256  std::string file_path =
257  FilePath::FromUTF8Unsafe(output_dir)
258  .Append(FilePath::FromUTF8Unsafe(playlist->file_name()))
259  .AsUTF8Unsafe();
260  if (!playlist->WriteToFile(file_path)) {
261  LOG(ERROR) << "Failed to write playlist " << file_path;
262  return false;
263  }
264  return true;
265 }
266 
267 } // namespace
268 
269 MediaPlaylistFactory::~MediaPlaylistFactory() {}
270 
271 std::unique_ptr<MediaPlaylist> MediaPlaylistFactory::Create(
272  const HlsParams& hls_params,
273  const std::string& file_name,
274  const std::string& name,
275  const std::string& group_id) {
276  return std::unique_ptr<MediaPlaylist>(
277  new MediaPlaylist(hls_params, file_name, name, group_id));
278 }
279 
281  : HlsNotifier(hls_params),
282  media_playlist_factory_(new MediaPlaylistFactory()) {
283  const base::FilePath master_playlist_path(
284  base::FilePath::FromUTF8Unsafe(hls_params.master_playlist_output));
285  master_playlist_dir_ = master_playlist_path.DirName().AsUTF8Unsafe();
286  const std::string& default_audio_langauge = hls_params.default_language;
287  const std::string& default_text_language =
291  master_playlist_.reset(
292  new MasterPlaylist(master_playlist_path.BaseName().AsUTF8Unsafe(),
293  default_audio_langauge, default_text_language,
294  hls_params.is_independent_segments));
295 }
296 
297 SimpleHlsNotifier::~SimpleHlsNotifier() {}
298 
300  return true;
301 }
302 
303 bool SimpleHlsNotifier::NotifyNewStream(const MediaInfo& media_info,
304  const std::string& playlist_name,
305  const std::string& name,
306  const std::string& group_id,
307  uint32_t* stream_id) {
308  DCHECK(stream_id);
309 
310  const std::string relative_playlist_path = MakePathRelative(
311  playlist_name, FilePath::FromUTF8Unsafe(master_playlist_dir_));
312 
313  std::unique_ptr<MediaPlaylist> media_playlist =
314  media_playlist_factory_->Create(hls_params(), relative_playlist_path,
315  name, group_id);
316  MediaInfo adjusted_media_info = MakeMediaInfoPathsRelativeToPlaylist(
317  media_info, hls_params().base_url, master_playlist_dir_,
318  media_playlist->file_name());
319  if (!media_playlist->SetMediaInfo(adjusted_media_info)) {
320  LOG(ERROR) << "Failed to set media info for playlist " << playlist_name;
321  return false;
322  }
323 
324  MediaPlaylist::EncryptionMethod encryption_method =
325  MediaPlaylist::EncryptionMethod::kNone;
326  if (media_info.protected_content().has_protection_scheme()) {
327  const std::string& protection_scheme =
328  media_info.protected_content().protection_scheme();
329  base::Optional<MediaPlaylist::EncryptionMethod> enc_method =
330  StringToEncryptionMethod(protection_scheme);
331  if (!enc_method) {
332  LOG(ERROR) << "Failed to recognize protection scheme "
333  << protection_scheme;
334  return false;
335  }
336  encryption_method = enc_method.value();
337  }
338 
339  base::AutoLock auto_lock(lock_);
340  *stream_id = sequence_number_++;
341  media_playlists_.push_back(media_playlist.get());
342  stream_map_[*stream_id].reset(
343  new StreamEntry{std::move(media_playlist), encryption_method});
344  return true;
345 }
346 
348  uint32_t sample_duration) {
349  base::AutoLock auto_lock(lock_);
350  auto stream_iterator = stream_map_.find(stream_id);
351  if (stream_iterator == stream_map_.end()) {
352  LOG(ERROR) << "Cannot find stream with ID: " << stream_id;
353  return false;
354  }
355  auto& media_playlist = stream_iterator->second->media_playlist;
356  media_playlist->SetSampleDuration(sample_duration);
357  return true;
358 }
359 
360 bool SimpleHlsNotifier::NotifyNewSegment(uint32_t stream_id,
361  const std::string& segment_name,
362  uint64_t start_time,
363  uint64_t duration,
364  uint64_t start_byte_offset,
365  uint64_t size) {
366  base::AutoLock auto_lock(lock_);
367  auto stream_iterator = stream_map_.find(stream_id);
368  if (stream_iterator == stream_map_.end()) {
369  LOG(ERROR) << "Cannot find stream with ID: " << stream_id;
370  return false;
371  }
372  auto& media_playlist = stream_iterator->second->media_playlist;
373  const std::string& segment_url =
374  GenerateSegmentUrl(segment_name, hls_params().base_url,
375  master_playlist_dir_, media_playlist->file_name());
376  media_playlist->AddSegment(segment_url, start_time, duration,
377  start_byte_offset, size);
378 
379  // Update target duration.
380  uint32_t longest_segment_duration =
381  static_cast<uint32_t>(ceil(media_playlist->GetLongestSegmentDuration()));
382  bool target_duration_updated = false;
383  if (longest_segment_duration > target_duration_) {
384  target_duration_ = longest_segment_duration;
385  target_duration_updated = true;
386  }
387 
388  // Update the playlists when there is new segments in live mode.
389  if (hls_params().playlist_type == HlsPlaylistType::kLive ||
390  hls_params().playlist_type == HlsPlaylistType::kEvent) {
391  // Update all playlists if target duration is updated.
392  if (target_duration_updated) {
393  for (MediaPlaylist* playlist : media_playlists_) {
394  playlist->SetTargetDuration(target_duration_);
395  if (!WriteMediaPlaylist(master_playlist_dir_, playlist))
396  return false;
397  }
398  } else {
399  if (!WriteMediaPlaylist(master_playlist_dir_, media_playlist.get()))
400  return false;
401  }
402  if (!master_playlist_->WriteMasterPlaylist(
403  hls_params().base_url, master_playlist_dir_, media_playlists_)) {
404  LOG(ERROR) << "Failed to write master playlist.";
405  return false;
406  }
407  }
408  return true;
409 }
410 
411 bool SimpleHlsNotifier::NotifyKeyFrame(uint32_t stream_id,
412  uint64_t timestamp,
413  uint64_t start_byte_offset,
414  uint64_t size) {
415  base::AutoLock auto_lock(lock_);
416  auto stream_iterator = stream_map_.find(stream_id);
417  if (stream_iterator == stream_map_.end()) {
418  LOG(ERROR) << "Cannot find stream with ID: " << stream_id;
419  return false;
420  }
421  auto& media_playlist = stream_iterator->second->media_playlist;
422  media_playlist->AddKeyFrame(timestamp, start_byte_offset, size);
423  return true;
424 }
425 
426 bool SimpleHlsNotifier::NotifyCueEvent(uint32_t stream_id, uint64_t timestamp) {
427  base::AutoLock auto_lock(lock_);
428  auto stream_iterator = stream_map_.find(stream_id);
429  if (stream_iterator == stream_map_.end()) {
430  LOG(ERROR) << "Cannot find stream with ID: " << stream_id;
431  return false;
432  }
433  auto& media_playlist = stream_iterator->second->media_playlist;
434  media_playlist->AddPlacementOpportunity();
435  return true;
436 }
437 
439  uint32_t stream_id,
440  const std::vector<uint8_t>& key_id,
441  const std::vector<uint8_t>& system_id,
442  const std::vector<uint8_t>& iv,
443  const std::vector<uint8_t>& protection_system_specific_data) {
444  base::AutoLock auto_lock(lock_);
445  auto stream_iterator = stream_map_.find(stream_id);
446  if (stream_iterator == stream_map_.end()) {
447  LOG(ERROR) << "Cannot find stream with ID: " << stream_id;
448  return false;
449  }
450 
451  std::unique_ptr<MediaPlaylist>& media_playlist =
452  stream_iterator->second->media_playlist;
453  const MediaPlaylist::EncryptionMethod encryption_method =
454  stream_iterator->second->encryption_method;
455  LOG_IF(WARNING, encryption_method == MediaPlaylist::EncryptionMethod::kNone)
456  << "Got encryption notification but the encryption method is NONE";
457  if (IsWidevineSystemId(system_id)) {
458  return HandleWidevineKeyFormats(encryption_method,
459  key_id, iv, protection_system_specific_data,
460  media_playlist.get());
461  }
462 
463  // Key Id does not need to be specified with "identity" and "sdk".
464  const std::vector<uint8_t> empty_key_id;
465 
466  if (IsCommonSystemId(system_id)) {
467  std::string key_uri = hls_params().key_uri;
468  if (key_uri.empty()) {
469  // Use key_id as the key_uri. The player needs to have custom logic to
470  // convert it to the actual key uri.
471  std::string key_uri_data = VectorToString(key_id);
472  key_uri = Base64EncodeData(kUriBase64Prefix, key_uri_data);
473  }
474  NotifyEncryptionToMediaPlaylist(encryption_method, key_uri, empty_key_id,
475  iv, "identity", "", media_playlist.get());
476  return true;
477  }
478  if (IsFairPlaySystemId(system_id)) {
479  std::string key_uri = hls_params().key_uri;
480  if (key_uri.empty()) {
481  // Use key_id as the key_uri. The player needs to have custom logic to
482  // convert it to the actual key uri.
483  std::string key_uri_data = VectorToString(key_id);
484  key_uri = Base64EncodeData(kUriFairPlayPrefix, key_uri_data);
485  }
486 
487  // FairPlay defines IV to be carried with the key, not the playlist.
488  const std::vector<uint8_t> empty_iv;
489  NotifyEncryptionToMediaPlaylist(encryption_method, key_uri, empty_key_id,
490  empty_iv, "com.apple.streamingkeydelivery",
491  "1", media_playlist.get());
492  return true;
493  }
494 
495  LOG(WARNING) << "HLS: Ignore unknown or unsupported system ID: "
496  << base::HexEncode(system_id.data(), system_id.size());
497  return true;
498 }
499 
501  base::AutoLock auto_lock(lock_);
502  for (MediaPlaylist* playlist : media_playlists_) {
503  playlist->SetTargetDuration(target_duration_);
504  if (!WriteMediaPlaylist(master_playlist_dir_, playlist))
505  return false;
506  }
507  if (!master_playlist_->WriteMasterPlaylist(
508  hls_params().base_url, master_playlist_dir_, media_playlists_)) {
509  LOG(ERROR) << "Failed to write master playlist.";
510  return false;
511  }
512  return true;
513 }
514 
515 } // namespace hls
516 } // namespace shaka
shaka::hls::MasterPlaylist
Definition: master_playlist.h:20
shaka::hls::HlsNotifier::hls_params
const HlsParams & hls_params() const
Definition: hls_notifier.h:104
shaka::HlsParams::default_text_language
std::string default_text_language
Definition: hls_params.h:53
shaka::hls::SimpleHlsNotifier::NotifyEncryptionUpdate
bool NotifyEncryptionUpdate(uint32_t stream_id, const std::vector< uint8_t > &key_id, const std::vector< uint8_t > &system_id, const std::vector< uint8_t > &iv, const std::vector< uint8_t > &protection_system_specific_data) override
Definition: simple_hls_notifier.cc:438
shaka::hls::SimpleHlsNotifier::NotifyKeyFrame
bool NotifyKeyFrame(uint32_t stream_id, uint64_t timestamp, uint64_t start_byte_offset, uint64_t size) override
Definition: simple_hls_notifier.cc:411
shaka::HlsParams
HLS related parameters.
Definition: hls_params.h:23
shaka::hls::SimpleHlsNotifier::Init
bool Init() override
Definition: simple_hls_notifier.cc:299
shaka::HlsParams::default_language
std::string default_language
Definition: hls_params.h:50
shaka
All the methods that are virtual are virtual for mocking.
Definition: gflags_hex_bytes.cc:11
shaka::HlsParams::playlist_type
HlsPlaylistType playlist_type
HLS playlist type. See HLS specification for details.
Definition: hls_params.h:25
shaka::hls::SimpleHlsNotifier::NotifyNewStream
bool NotifyNewStream(const MediaInfo &media_info, const std::string &playlist_name, const std::string &stream_name, const std::string &group_id, uint32_t *stream_id) override
Definition: simple_hls_notifier.cc:303
shaka::HlsParams::key_uri
std::string key_uri
Definition: hls_params.h:44
shaka::hls::HlsNotifier
Definition: hls_notifier.h:20
shaka::HlsParams::master_playlist_output
std::string master_playlist_output
HLS master playlist output path.
Definition: hls_params.h:27
shaka::hls::MediaPlaylistFactory
Definition: simple_hls_notifier.h:28
shaka::hls::SimpleHlsNotifier::NotifyCueEvent
bool NotifyCueEvent(uint32_t container_id, uint64_t timestamp) override
Definition: simple_hls_notifier.cc:426
shaka::hls::SimpleHlsNotifier::SimpleHlsNotifier
SimpleHlsNotifier(const HlsParams &hls_params)
Definition: simple_hls_notifier.cc:280
shaka::hls::SimpleHlsNotifier::NotifyNewSegment
bool NotifyNewSegment(uint32_t stream_id, const std::string &segment_name, uint64_t start_time, uint64_t duration, uint64_t start_byte_offset, uint64_t size) override
Definition: simple_hls_notifier.cc:360
shaka::hls::SimpleHlsNotifier::Flush
bool Flush() override
Definition: simple_hls_notifier.cc:500
shaka::hls::SimpleHlsNotifier::NotifySampleDuration
bool NotifySampleDuration(uint32_t stream_id, uint32_t sample_duration) override
Definition: simple_hls_notifier.cc:347
shaka::hls::MediaPlaylist
Methods are virtual for mocking.
Definition: media_playlist.h:47
shaka::media::PsshBoxBuilder::ParseFromBox
static std::unique_ptr< PsshBoxBuilder > ParseFromBox(const uint8_t *data, size_t data_size)
Definition: protection_system_specific_info.cc:66