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/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
25ABSL_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
32namespace shaka {
33
34namespace hls {
35
36namespace {
37
38const char kUriBase64Prefix[] = "data:text/plain;base64,";
39const char kUriBase64Utf16Prefix[] = "data:text/plain;charset=UTF-16;base64,";
40const char kUriFairPlayPrefix[] = "skd://";
41const char kWidevineDashIfIopUUID[] =
42 "urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed";
43
44bool 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
50bool 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
55bool 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
61bool 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
67bool 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
73std::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
80std::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.
86std::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
103MediaInfo 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
127bool 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
176std::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
191void 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.
219bool 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
251bool 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
263MediaPlaylistFactory::~MediaPlaylistFactory() {}
264
265std::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
274SimpleHlsNotifier::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
291SimpleHlsNotifier::~SimpleHlsNotifier() {}
292
294 return true;
295}
296
297bool 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
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
405bool 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
420bool 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
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.