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