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