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