diff --git a/packager/app/fixed_key_encryption_flags.cc b/packager/app/fixed_key_encryption_flags.cc index b84ae5f180..7b6d383d42 100644 --- a/packager/app/fixed_key_encryption_flags.cc +++ b/packager/app/fixed_key_encryption_flags.cc @@ -25,8 +25,8 @@ DEFINE_string(iv, DEFINE_string(pssh, "", "One or more PSSH boxes in hex string format. If not specified, " - "will generate a v1 common PSSH box according to " - "https://goo.gl/507mKp."); + "will generate a v1 common PSSH box as specified in " + "https://goo.gl/s8RIhr."); namespace shaka { diff --git a/packager/app/widevine_encryption_flags.cc b/packager/app/widevine_encryption_flags.cc index 7abc4eb6ac..3069acb7f5 100644 --- a/packager/app/widevine_encryption_flags.cc +++ b/packager/app/widevine_encryption_flags.cc @@ -27,7 +27,7 @@ DEFINE_bool(include_common_pssh, false, "When using Widevine encryption, include an additional v1 PSSH box " "for the common system ID that includes the key IDs. See: " - "https://goo.gl/507mKp"); + "https://goo.gl/s8RIhr"); DEFINE_string(key_server_url, "", "Key server url. Required for encryption and " "decryption"); DEFINE_string(content_id, "", "Content Id (hex)."); diff --git a/packager/packager.cc b/packager/packager.cc new file mode 100644 index 0000000000..bc0b61f377 --- /dev/null +++ b/packager/packager.cc @@ -0,0 +1,683 @@ +// Copyright 2017 Google Inc. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +#include "packager/packager.h" + +#include +#include + +#include "packager/app/libcrypto_threading.h" +#include "packager/app/packager_util.h" +#include "packager/app/stream_descriptor.h" +#include "packager/base/files/file_path.h" +#include "packager/base/logging.h" +#include "packager/base/path_service.h" +#include "packager/base/strings/stringprintf.h" +#include "packager/base/threading/simple_thread.h" +#include "packager/base/time/clock.h" +#include "packager/hls/base/hls_notifier.h" +#include "packager/hls/base/simple_hls_notifier.h" +#include "packager/media/base/container_names.h" +#include "packager/media/base/fourccs.h" +#include "packager/media/base/key_source.h" +#include "packager/media/base/muxer_options.h" +#include "packager/media/base/muxer_util.h" +#include "packager/media/chunking/chunking_handler.h" +#include "packager/media/crypto/encryption_handler.h" +#include "packager/media/demuxer/demuxer.h" +#include "packager/media/event/hls_notify_muxer_listener.h" +#include "packager/media/event/mpd_notify_muxer_listener.h" +#include "packager/media/event/vod_media_info_dump_muxer_listener.h" +#include "packager/media/file/file.h" +#include "packager/media/formats/mp2t/ts_muxer.h" +#include "packager/media/formats/mp4/mp4_muxer.h" +#include "packager/media/formats/webm/webm_muxer.h" +#include "packager/media/trick_play/trick_play_handler.h" +#include "packager/mpd/base/dash_iop_mpd_notifier.h" +#include "packager/mpd/base/media_info.pb.h" +#include "packager/mpd/base/mpd_builder.h" +#include "packager/mpd/base/simple_mpd_notifier.h" + +DEFINE_bool(dump_stream_info, false, "Dump demuxed stream info."); +DEFINE_bool(use_fake_clock_for_muxer, + false, + "Set to true to use a fake clock for muxer. With this flag set, " + "creation time and modification time in outputs are set to 0. " + "Should only be used for testing."); + +namespace shaka { + +// TODO(kqyang): Clean up namespaces. +using media::ChunkingOptions; +using media::Demuxer; +using media::EncryptionOptions; +using media::KeySource; +using media::MuxerOptions; +using media::Status; +namespace error = media::error; + +namespace media { +namespace { + +const char kMediaInfoSuffix[] = ".media_info"; + +// TODO(rkuroiwa): Write TTML and WebVTT parser (demuxing) for a better check +// and for supporting live/segmenting (muxing). With a demuxer and a muxer, +// CreateRemuxJobs() shouldn't treat text as a special case. +std::string DetermineTextFileFormat(const std::string& file) { + std::string content; + if (!File::ReadFileToString(file.c_str(), &content)) { + LOG(ERROR) << "Failed to open file " << file + << " to determine file format."; + return ""; + } + MediaContainerName container_name = DetermineContainer( + reinterpret_cast(content.data()), content.size()); + if (container_name == CONTAINER_WEBVTT) { + return "vtt"; + } else if (container_name == CONTAINER_TTML) { + return "ttml"; + } + + return ""; +} + +MediaContainerName GetOutputFormat(const StreamDescriptor& descriptor) { + MediaContainerName output_format = CONTAINER_UNKNOWN; + if (!descriptor.output_format.empty()) { + output_format = DetermineContainerFromFormatName(descriptor.output_format); + if (output_format == CONTAINER_UNKNOWN) { + LOG(ERROR) << "Unable to determine output format from '" + << descriptor.output_format << "'."; + } + } else { + const std::string& output_name = descriptor.output.empty() + ? descriptor.segment_template + : descriptor.output; + if (output_name.empty()) + return CONTAINER_UNKNOWN; + output_format = DetermineContainerFromFileName(output_name); + if (output_format == CONTAINER_UNKNOWN) { + LOG(ERROR) << "Unable to determine output format from '" << output_name + << "'."; + } + } + return output_format; +} + +bool ValidateStreamDescriptor(bool dump_stream_info, + const StreamDescriptor& descriptor) { + // Validate and insert the descriptor + if (descriptor.input.empty()) { + LOG(ERROR) << "Stream input not specified."; + return false; + } + if (!dump_stream_info && descriptor.stream_selector.empty()) { + LOG(ERROR) << "Stream stream_selector not specified."; + return false; + } + + // We should have either output or segment_template specified. + const bool output_specified = + !descriptor.output.empty() || !descriptor.segment_template.empty(); + if (!output_specified) { + if (!FLAGS_dump_stream_info) { + LOG(ERROR) << "Stream output not specified."; + return false; + } + } else { + const MediaContainerName output_format = GetOutputFormat(descriptor); + if (output_format == CONTAINER_UNKNOWN) + return false; + + if (output_format == MediaContainerName::CONTAINER_MPEG2TS) { + if (descriptor.segment_template.empty()) { + LOG(ERROR) << "Please specify segment_template. Single file TS output " + "is not supported."; + return false; + } + // Note that MPEG2 TS doesn't need a separate initialization segment, so + // output field is not needed. + if (!descriptor.output.empty()) { + LOG(WARNING) << "TS init_segment '" << descriptor.output + << "' ignored. TS muxer does not support initialization " + "segment generation."; + } + } else { + if (descriptor.output.empty()) { + LOG(ERROR) << "init_segment is required for format " << output_format; + return false; + } + } + } + return true; +} + +bool ValidateParams(const PackagingParams& packaging_params, + const std::vector& stream_descriptors) { + if (!packaging_params.chunking_params.segment_sap_aligned && + packaging_params.chunking_params.subsegment_sap_aligned) { + LOG(ERROR) << "Setting segment_sap_aligned to false but " + "subsegment_sap_aligned to true is not allowed."; + return false; + } + + if (packaging_params.output_media_info && + !packaging_params.mpd_params.mpd_output.empty()) { + LOG(ERROR) << "output_media_info and MPD output do not work together."; + return false; + } + + if (packaging_params.output_media_info && + !packaging_params.hls_params.master_playlist_output.empty()) { + LOG(ERROR) << "output_media_info and HLS output do not work together."; + return false; + } + + // Since there isn't a muxer listener that can output both MPD and HLS, + // disallow specifying both MPD and HLS flags. + if (!packaging_params.mpd_params.mpd_output.empty() && + !packaging_params.hls_params.master_playlist_output.empty()) { + LOG(ERROR) << "output both MPD and HLS are not supported."; + return false; + } + + if (stream_descriptors.empty()) { + LOG(ERROR) << "Stream descriptors cannot be empty."; + return false; + } + + // On demand profile generates single file segment while live profile + // generates multiple segments specified using segment template. + const bool on_demand_dash_profile = + stream_descriptors.begin()->segment_template.empty(); + for (const auto& descriptor : stream_descriptors) { + if (on_demand_dash_profile != descriptor.segment_template.empty()) { + LOG(ERROR) << "Inconsistent stream descriptor specification: " + "segment_template should be specified for none or all " + "stream descriptors."; + return false; + } + if (!ValidateStreamDescriptor(FLAGS_dump_stream_info, descriptor)) + return false; + } + if (packaging_params.output_media_info && !on_demand_dash_profile) { + // TODO(rkuroiwa, kqyang): Support partial media info dump for live. + NOTIMPLEMENTED() << "ERROR: --output_media_info is only supported for " + "on-demand profile (not using segment_template)."; + return false; + } + + return true; +} + +class StreamDescriptorCompareFn { + public: + bool operator()(const StreamDescriptor& a, const StreamDescriptor& b) { + if (a.input == b.input) { + if (a.stream_selector == b.stream_selector) + // Stream with high trick_play_factor is at the beginning. + return a.trick_play_factor > b.trick_play_factor; + else + return a.stream_selector < b.stream_selector; + } + + return a.input < b.input; + } +}; + +/// Sorted list of StreamDescriptor. +typedef std::multiset + StreamDescriptorList; + +// A fake clock that always return time 0 (epoch). Should only be used for +// testing. +class FakeClock : public base::Clock { + public: + base::Time Now() override { return base::Time(); } +}; + +// Demux, Mux(es) and worker thread used to remux a source file/stream. +class RemuxJob : public base::SimpleThread { + public: + RemuxJob(std::unique_ptr demuxer) + : SimpleThread("RemuxJob"), demuxer_(std::move(demuxer)) {} + + ~RemuxJob() override {} + + Demuxer* demuxer() { return demuxer_.get(); } + Status status() { return status_; } + + private: + RemuxJob(const RemuxJob&) = delete; + RemuxJob& operator=(const RemuxJob&) = delete; + + void Run() override { + DCHECK(demuxer_); + status_ = demuxer_->Run(); + } + + std::unique_ptr demuxer_; + Status status_; +}; + +bool StreamInfoToTextMediaInfo(const StreamDescriptor& stream_descriptor, + const MuxerOptions& stream_muxer_options, + MediaInfo* text_media_info) { + const std::string& language = stream_descriptor.language; + const std::string format = DetermineTextFileFormat(stream_descriptor.input); + if (format.empty()) { + LOG(ERROR) << "Failed to determine the text file format for " + << stream_descriptor.input; + return false; + } + + if (!File::Copy(stream_descriptor.input.c_str(), + stream_muxer_options.output_file_name.c_str())) { + LOG(ERROR) << "Failed to copy the input file (" << stream_descriptor.input + << ") to output file (" << stream_muxer_options.output_file_name + << ")."; + return false; + } + + text_media_info->set_media_file_name(stream_muxer_options.output_file_name); + text_media_info->set_container_type(MediaInfo::CONTAINER_TEXT); + + if (stream_muxer_options.bandwidth != 0) { + text_media_info->set_bandwidth(stream_muxer_options.bandwidth); + } else { + // Text files are usually small and since the input is one file; there's no + // way for the player to do ranged requests. So set this value to something + // reasonable. + const int kDefaultTextBandwidth = 256; + text_media_info->set_bandwidth(kDefaultTextBandwidth); + } + + MediaInfo::TextInfo* text_info = text_media_info->mutable_text_info(); + text_info->set_format(format); + if (!language.empty()) + text_info->set_language(language); + + return true; +} + +std::shared_ptr CreateOutputMuxer(const MuxerOptions& options, + MediaContainerName container) { + if (container == CONTAINER_WEBM) { + return std::shared_ptr(new webm::WebMMuxer(options)); + } else if (container == CONTAINER_MPEG2TS) { + return std::shared_ptr(new mp2t::TsMuxer(options)); + } else { + DCHECK_EQ(container, CONTAINER_MOV); + return std::shared_ptr(new mp4::MP4Muxer(options)); + } +} + +bool CreateRemuxJobs(const StreamDescriptorList& stream_descriptors, + const PackagingParams& packaging_params, + const ChunkingOptions& chunking_options, + const EncryptionOptions& encryption_options, + const MuxerOptions& muxer_options, + FakeClock* fake_clock, + KeySource* encryption_key_source, + MpdNotifier* mpd_notifier, + hls::HlsNotifier* hls_notifier, + std::vector>* remux_jobs) { + // No notifiers OR (mpd_notifier XOR hls_notifier); which is NAND. + DCHECK(!(mpd_notifier && hls_notifier)); + DCHECK(remux_jobs); + + std::shared_ptr trick_play_handler; + + std::string previous_input; + std::string previous_stream_selector; + int stream_number = 0; + for (StreamDescriptorList::const_iterator + stream_iter = stream_descriptors.begin(); + stream_iter != stream_descriptors.end(); + ++stream_iter, ++stream_number) { + MediaContainerName output_format = GetOutputFormat(*stream_iter); + + // Process stream descriptor. + MuxerOptions stream_muxer_options(muxer_options); + stream_muxer_options.output_file_name = stream_iter->output; + if (!stream_iter->segment_template.empty()) { + if (!ValidateSegmentTemplate(stream_iter->segment_template)) { + LOG(ERROR) << "ERROR: segment template with '" + << stream_iter->segment_template << "' is invalid."; + return false; + } + stream_muxer_options.segment_template = stream_iter->segment_template; + } + stream_muxer_options.bandwidth = stream_iter->bandwidth; + + if (stream_iter->stream_selector == "text" && + output_format != CONTAINER_MOV) { + MediaInfo text_media_info; + if (!StreamInfoToTextMediaInfo(*stream_iter, stream_muxer_options, + &text_media_info)) { + return false; + } + + if (mpd_notifier) { + uint32_t unused; + if (!mpd_notifier->NotifyNewContainer(text_media_info, &unused)) { + LOG(ERROR) << "Failed to process text file " << stream_iter->input; + } else { + mpd_notifier->Flush(); + } + } else if (packaging_params.output_media_info) { + VodMediaInfoDumpMuxerListener::WriteMediaInfoToFile( + text_media_info, + stream_muxer_options.output_file_name + kMediaInfoSuffix); + } + continue; + } + + if (stream_iter->input != previous_input) { + // New remux job needed. Create demux and job thread. + std::unique_ptr demuxer(new Demuxer(stream_iter->input)); + demuxer->set_dump_stream_info(FLAGS_dump_stream_info); + if (packaging_params.decryption_params.key_provider != + KeyProvider::kNone) { + std::unique_ptr decryption_key_source( + CreateDecryptionKeySource(packaging_params.decryption_params)); + if (!decryption_key_source) + return false; + demuxer->SetKeySource(std::move(decryption_key_source)); + } + remux_jobs->emplace_back(new RemuxJob(std::move(demuxer))); + trick_play_handler.reset(); + previous_input = stream_iter->input; + // Skip setting up muxers if output is not needed. + if (stream_iter->output.empty() && stream_iter->segment_template.empty()) + continue; + } + DCHECK(!remux_jobs->empty()); + + // Each stream selector requires an individual trick play handler. + // E.g., an input with two video streams needs two trick play handlers. + // TODO(hmchen): add a test case in packager_test.py for two video streams + // input. + if (stream_iter->stream_selector != previous_stream_selector) { + previous_stream_selector = stream_iter->stream_selector; + trick_play_handler.reset(); + } + + std::shared_ptr muxer( + CreateOutputMuxer(stream_muxer_options, output_format)); + if (FLAGS_use_fake_clock_for_muxer) muxer->set_clock(fake_clock); + + std::unique_ptr muxer_listener; + DCHECK(!(packaging_params.output_media_info && mpd_notifier)); + if (packaging_params.output_media_info) { + const std::string output_media_info_file_name = + stream_muxer_options.output_file_name + kMediaInfoSuffix; + std::unique_ptr + vod_media_info_dump_muxer_listener( + new VodMediaInfoDumpMuxerListener(output_media_info_file_name)); + muxer_listener = std::move(vod_media_info_dump_muxer_listener); + } + if (mpd_notifier) { + std::unique_ptr mpd_notify_muxer_listener( + new MpdNotifyMuxerListener(mpd_notifier)); + muxer_listener = std::move(mpd_notify_muxer_listener); + } + + if (hls_notifier) { + // TODO(rkuroiwa): Do some smart stuff to group the audios, e.g. detect + // languages. + std::string group_id = stream_iter->hls_group_id; + std::string name = stream_iter->hls_name; + std::string hls_playlist_name = stream_iter->hls_playlist_name; + if (group_id.empty()) + group_id = "audio"; + if (name.empty()) + name = base::StringPrintf("stream_%d", stream_number); + if (hls_playlist_name.empty()) + hls_playlist_name = base::StringPrintf("stream_%d.m3u8", stream_number); + + muxer_listener.reset(new HlsNotifyMuxerListener(hls_playlist_name, name, + group_id, hls_notifier)); + } + + if (muxer_listener) + muxer->SetMuxerListener(std::move(muxer_listener)); + + // Create a new trick_play_handler. Note that the stream_decriptors + // are sorted so that for the same input and stream_selector, the main + // stream is always the last one following the trick play streams. + if (stream_iter->trick_play_factor > 0) { + if (!trick_play_handler) { + trick_play_handler.reset(new TrickPlayHandler()); + } + trick_play_handler->SetHandlerForTrickPlay(stream_iter->trick_play_factor, + std::move(muxer)); + if (trick_play_handler->IsConnected()) + continue; + } else if (trick_play_handler) { + trick_play_handler->SetHandlerForMainStream(std::move(muxer)); + DCHECK(trick_play_handler->IsConnected()); + continue; + } + + std::vector> handlers; + + auto chunking_handler = std::make_shared(chunking_options); + handlers.push_back(chunking_handler); + + Status status; + if (encryption_key_source && !stream_iter->skip_encryption) { + auto new_encryption_options = encryption_options; + // Use Sample AES in MPEG2TS. + // TODO(kqyang): Consider adding a new flag to enable Sample AES as we + // will support CENC in TS in the future. + if (output_format == CONTAINER_MPEG2TS) { + VLOG(1) << "Use Apple Sample AES encryption for MPEG2TS."; + new_encryption_options.protection_scheme = + kAppleSampleAesProtectionScheme; + } + handlers.emplace_back( + new EncryptionHandler(new_encryption_options, encryption_key_source)); + } + + // If trick_play_handler is available, muxer should already be connected to + // trick_play_handler. + if (trick_play_handler) { + handlers.push_back(trick_play_handler); + } else { + handlers.push_back(std::move(muxer)); + } + + auto* demuxer = remux_jobs->back()->demuxer(); + const std::string& stream_selector = stream_iter->stream_selector; + status.Update(demuxer->SetHandler(stream_selector, chunking_handler)); + status.Update(ConnectHandlers(handlers)); + + if (!status.ok()) { + LOG(ERROR) << "Failed to setup graph: " << status; + return false; + } + if (!stream_iter->language.empty()) + demuxer->SetLanguageOverride(stream_selector, stream_iter->language); + } + + // Initialize processing graph. + for (const std::unique_ptr& job : *remux_jobs) { + Status status = job->demuxer()->Initialize(); + if (!status.ok()) { + LOG(ERROR) << "Failed to initialize processing graph " << status; + return false; + } + } + return true; +} + +Status RunRemuxJobs(const std::vector>& remux_jobs) { + // Start the job threads. + for (const std::unique_ptr& job : remux_jobs) + job->Start(); + + // Wait for all jobs to complete or an error occurs. + Status status; + bool all_joined; + do { + all_joined = true; + for (const std::unique_ptr& job : remux_jobs) { + if (job->HasBeenJoined()) { + status = job->status(); + if (!status.ok()) + break; + } else { + all_joined = false; + job->Join(); + } + } + } while (!all_joined && status.ok()); + + return status; +} + +} // namespace +} // namespace media + +std::string EncryptionParams::DefaultStreamLabelFunction( + int max_sd_pixels, + int max_hd_pixels, + int max_uhd1_pixels, + const EncryptedStreamAttributes& stream_attributes) { + if (stream_info.stream_type == EncryptedStreamAttributes::kAudio) + return "AUDIO"; + if (stream_info.stream_type == EncryptedStreamAttributes::kVideo) { + const int pixels = stream_attributes.oneof.video.width * + stream_attributes.oneof.video.height; + if (pixels <= max_sd_pixels) return "SD"; + if (pixels <= max_hd_pixels) return "HD"; + if (pixels <= max_uhd1_pixels) return "UHD1"; + return "UHD2"; + } + return ""; +} + +struct ShakaPackager::PackagerInternal { + media::FakeClock fake_clock; + std::unique_ptr encryption_key_source; + std::unique_ptr mpd_notifier; + std::unique_ptr hls_notifier; + std::vector> remux_jobs; +}; + +ShakaPackager::ShakaPackager() {} + +ShakaPackager::~ShakaPackager() {} + +Status ShakaPackager::Initialize( + const PackagingParams& packaging_params, + const std::vector& stream_descriptors) { + + static media::LibcryptoThreading libcrypto_threading; + + if (internal_) + return Status(error::INVALID_ARGUMENT, "Already initialized."); + + if (!media::ValidateParams(packaging_params, stream_descriptors)) + return Status(error::INVALID_ARGUMENT, "Invalid packaging params."); + + std::unique_ptr internal(new PackagerInternal); + + ChunkingOptions chunking_options = + media::GetChunkingOptions(packaging_params.chunking_params); + EncryptionOptions encryption_options = + media::GetEncryptionOptions(packaging_params.encryption_params); + MuxerOptions muxer_options = media::GetMuxerOptions( + packaging_params.temp_dir, packaging_params.mp4_output_params); + + const bool on_demand_dash_profile = + stream_descriptors.begin()->segment_template.empty(); + MpdOptions mpd_options = + media::GetMpdOptions(on_demand_dash_profile, packaging_params.mpd_params); + + // Create encryption key source if needed. + if (packaging_params.encryption_params.key_provider != KeyProvider::kNone) { + if (encryption_options.protection_scheme == media::FOURCC_NULL) + return Status(error::INVALID_ARGUMENT, "Invalid protection scheme."); + internal->encryption_key_source = + CreateEncryptionKeySource(encryption_options.protection_scheme, + packaging_params.encryption_params); + if (!internal->encryption_key_source) + return Status(error::INVALID_ARGUMENT, "Failed to create key source."); + } + + const MpdParams& mpd_params = packaging_params.mpd_params; + if (!mpd_params.mpd_output.empty()) { + if (mpd_params.generate_dash_if_iop_compliant_mpd) { + internal->mpd_notifier.reset(new DashIopMpdNotifier( + mpd_options, mpd_params.base_urls, mpd_params.mpd_output)); + } else { + internal->mpd_notifier.reset(new SimpleMpdNotifier( + mpd_options, mpd_params.base_urls, mpd_params.mpd_output)); + } + if (!internal->mpd_notifier->Init()) { + LOG(ERROR) << "MpdNotifier failed to initialize."; + return Status(error::INVALID_ARGUMENT, + "Failed to initialize MpdNotifier."); + } + } + + const HlsParams& hls_params = packaging_params.hls_params; + if (!hls_params.master_playlist_output.empty()) { + base::FilePath master_playlist_path( + base::FilePath::FromUTF8Unsafe(hls_params.master_playlist_output)); + base::FilePath master_playlist_name = master_playlist_path.BaseName(); + + internal->hls_notifier.reset(new hls::SimpleHlsNotifier( + hls::HlsNotifier::HlsProfile::kOnDemandProfile, hls_params.base_url, + master_playlist_path.DirName().AsEndingWithSeparator().AsUTF8Unsafe(), + master_playlist_name.AsUTF8Unsafe())); + } + + media::StreamDescriptorList stream_descriptor_list; + for (const StreamDescriptor& descriptor : stream_descriptors) + stream_descriptor_list.insert(descriptor); + if (!media::CreateRemuxJobs( + stream_descriptor_list, packaging_params, chunking_options, + encryption_options, muxer_options, &internal->fake_clock, + internal->encryption_key_source.get(), internal->mpd_notifier.get(), + internal->hls_notifier.get(), &internal->remux_jobs)) { + return Status(error::INVALID_ARGUMENT, "Failed to create remux jobs."); + } + internal_ = std::move(internal); + return Status::OK; +} + +Status ShakaPackager::Run() { + if (!internal_) + return Status(error::INVALID_ARGUMENT, "Not yet initialized."); + Status status = media::RunRemuxJobs(internal_->remux_jobs); + if (!status.ok()) + return status; + + if (internal_->hls_notifier) { + if (!internal_->hls_notifier->Flush()) + return Status(error::INVALID_ARGUMENT, "Failed to flush Hls."); + } + if (internal_->mpd_notifier) { + if (!internal_->mpd_notifier->Flush()) + return Status(error::INVALID_ARGUMENT, "Failed to flush Mpd."); + } + return Status::OK; +} + +void ShakaPackager::Cancel() { + if (!internal_) { + LOG(INFO) << "Not yet initialized. Return directly."; + return; + } + for (const std::unique_ptr& job : internal_->remux_jobs) + job->demuxer()->Cancel(); +} + +} // namespace shaka diff --git a/packager/packager.h b/packager/packager.h new file mode 100644 index 0000000000..6d7469e07a --- /dev/null +++ b/packager/packager.h @@ -0,0 +1,427 @@ +// Copyright 2017 Google Inc. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +#ifndef PACKAGER_PACKAGER_H_ +#define PACKAGER_PACKAGER_H_ + +#include +#include +#include +#include +#include + +// TODO(kqyang): Refactor status.h and move it under packager/. +#include "packager/media/base/status.h" + +namespace shaka { + +/// MP4 (ISO-BMFF) output related parameters. +struct Mp4OutputParams { + // Include pssh in the encrypted stream. CMAF recommends carrying + // license acquisition information in the manifest and not duplicate the + // information in the stream. (This is not a hard requirement so we are still + // CMAF compatible even if pssh is included in the stream.) + bool include_pssh_in_stream = true; + /// Set the number of subsegments in each SIDX box. If 0, a single SIDX box + /// is used per segment. If -1, no SIDX box is used. Otherwise, the Muxer + /// will pack N subsegments in the root SIDX of the segment, with + /// segment_duration/N/subsegment_duration fragments per subsegment. + /// This flag is ingored for DASH MPD with on-demand profile. + const int kNoSidxBoxInSegment = -1; + const int kSingleSidxPerSegment = 0; + int num_subsegments_per_sidx = kSingleSidxPerSegment; + /// Set the flag use_decoding_timestamp_in_timeline, which if set to true, use + /// decoding timestamp instead of presentation timestamp in media timeline, + /// which is needed to workaround a Chromium bug that decoding timestamp is + /// used in buffered range, https://crbug.com/398130. + bool use_decoding_timestamp_in_timeline = false; +}; + +/// Chunking (segmentation) related parameters. +struct ChunkingParams { + /// Segment duration in seconds. + double segment_duration_in_seconds = 0; + /// Subsegment duration in seconds. Should not be larger than the segment + /// duration. + double subsegment_duration_in_seconds = 0; + + /// Force segments to begin with stream access points. Actual segment duration + /// may not be exactly what is specified by segment_duration. + bool segment_sap_aligned = true; + /// Force subsegments to begin with stream access points. Actual subsegment + /// duration may not be exactly what is specified by subsegment_duration. + /// Setting to subsegment_sap_aligned to true but segment_sap_aligned to false + /// is not allowed. + bool subsegment_sap_aligned = true; +}; + +/// DASH MPD related parameters. +struct MpdParams { + /// MPD output file path. + std::string mpd_output; + /// BaseURLs for the MPD. The values will be added as element(s) + /// under the element. + std::vector base_urls; + /// Set MPD@minBufferTime attribute, which specifies, in seconds, a common + /// duration used in the definition of the MPD representation data rate. A + /// client can be assured of having enough data for continous playout + /// providing playout begins at min_buffer_time after the first bit is + /// received. + double min_buffer_time = 2.0; + /// Generate static MPD for live profile. Note that this flag has no effect + /// for on-demand profile, in which case static MPD is always used. + bool generate_static_live_mpd = false; + /// Set MPD@timeShiftBufferDepth attribute, which is the guaranteed duration + /// of the time shifting buffer for 'dynamic' media presentations, in seconds. + double time_shift_buffer_depth = 0; + /// Set MPD@suggestedPresentationDelay attribute. For 'dynamic' media + /// presentations, it specifies a delay, in seconds, to be added to the media + /// presentation time. The attribute is not set if the value is 0; the client + /// is expected to choose a suitable value in this case. + const double kSuggestedPresentationDelayNotSet = 0; + double suggested_presentation_delay = kSuggestedPresentationDelayNotSet; + /// Set MPD@minimumUpdatePeriod attribute, which indicates to the player how + /// often to refresh the MPD in seconds. For dynamic MPD only. + double minimum_update_period = 0; + /// The tracks tagged with this language will have + /// in the manifest. This allows the player to choose the correct default + /// language for the content. + std::string default_language; + /// Try to generate DASH-IF IOP compliant MPD. + bool generate_dash_if_iop_compliant_mpd = true; +}; + +/// HLS related parameters. +struct HlsParams { + /// HLS master playlist output path. + std::string master_playlist_output; + /// The base URL for the Media Playlists and media files listed in the + /// playlists. This is the prefix for the files. + std::string base_url; +}; + +/// Encryption / decryption key providers. +enum class KeyProvider { + kNone = 0, + kWidevine = 1, + kPlayready = 2, + kRawKey = 3, +}; + +/// Signer credential for Widevine license server. +struct WidevineSigner { + /// Name of the signer / content provider. + std::string signer_name; + + enum class SigningKeyType { + kNone, + kAes, + kRsa, + }; + /// Specifies the signing key type, which determines whether AES or RSA key + /// are used to authenticate the signer. A type of 'kNone' is invalid. + SigningKeyType signing_key_type = SigningKeyType::kNone; + struct { + /// AES signing key. + std::string key; + /// AES signing IV. + std::string iv; + } aes; + struct { + /// RSA signing private key. + std::string key; + } rsa; +}; + +/// Widevine encryption parameters. +struct WidevineEncryptionParams { + /// Widevine license / key server URL. + std::string key_server_url; + /// Generates and includes an additional v1 PSSH box for the common system ID. + /// See: https://goo.gl/s8RIhr. + // TODO(kqyang): Move to EncryptionParams and support common PSSH generation + // in all key providers. + bool include_common_pssh = false; + /// Content identifier. + std::vector content_id; + /// The name of a stored policy, which specifies DRM content rights. + std::string policy; + /// Signer credential for Widevine license / key server. + WidevineSigner signer; +}; + +/// Playready encryption parameters. +/// Two different modes of playready key acquisition is supported: +/// (1) Fetch from a key server. `key_server_url` and `program_identifier` are +/// required. The presence of other parameters may be necessary depends +/// on server configuration. +/// (2) Provide the raw key directly. Both `key_id` and `key` are required. +/// We are planning to merge this mode with `RawKeyEncryptionParams`. +struct PlayreadyEncryptionParams { + /// Playready license / key server URL. + std::string key_server_url; + /// Playready program identifier. + std::string program_identifier; + /// Absolute path to the Certificate Authority file for the server cert in PEM + /// format. + std::string ca_file; + /// Absolute path to client certificate file. + std::string client_cert_file; + /// Absolute path to the private key file. + std::string client_cert_private_key_file; + /// Password to the private key file. + std::string client_cert_private_key_password; + + // TODO(kqyang): move raw playready key generation to RawKey. + /// Provides a raw Playready KeyId. + std::string key_id; + /// Provides a raw Playready Key. + std::string key; +}; + +/// Raw key encryption parameters, i.e. with key parameters provided. +struct RawKeyEncryptionParams { + /// An optional initialization vector. If not provided, a random `iv` will be + /// generated. Note that this parameter should only be used during testing. + std::string iv; + /// Inject a custom `pssh` or multiple concatenated `psshs`. If not provided, + /// a common system pssh will be generated. + std::string pssh; + + using StreamLabel = std::string; + struct KeyPair { + std::string key_id; + std::string key; + }; + /// Defines the KeyPair for the streams. An empty `StreamLabel` indicates the + /// default `KeyPair`, which applies to all the `StreamLabels` not present in + /// `key_map`. + std::map key_map; +}; + +/// Encryption parameters. +struct EncryptionParams { + /// Specifies the key provider, which determines which key provider is used + /// and which encryption params is valid. 'kNone' means not to encrypt the + /// streams. + KeyProvider key_provider = KeyProvider::kNone; + // Only one of the three fields is valid. + WidevineEncryptionParams widevine; + PlayreadyEncryptionParams playready; + RawKeyEncryptionParams raw_key; + + /// Clear lead duration in seconds. + double clear_lead_in_seconds = 0; + /// The protection scheme: "cenc", "cens", "cbc1", "cbcs". + std::string protection_scheme = "cenc"; + /// Crypto period duration in seconds. A positive value means key rotation is + /// enabled, the key provider must support key rotation in this case. + const double kNoKeyRotation = 0; + double crypto_period_duration_in_seconds = 0; + /// Enable/disable subsample encryption for VP9. + bool vp9_subsample_encryption = true; + + /// Encrypted stream information that is used to determine stream label. + struct EncryptedStreamAttributes { + enum StreamType { + kUnknown, + kVideo, + kAudio, + }; + + StreamType stream_type = kUnknown; + union OneOf { + OneOf() {} + + struct { + int width = 0; + int height = 0; + float frame_rate = 0; + int bit_depth = 0; + } video; + + struct { + int number_of_channels = 0; + } audio; + } oneof; + }; + /// Default stream label function implementation. + /// @param max_sd_pixels The threshold to determine whether a video track + /// should be considered as SD. If the max pixels per + /// frame is no higher than max_sd_pixels, i.e. [0, + /// max_sd_pixels], it is SD. + /// @param max_hd_pixels: The threshold to determine whether a video track + /// should be considered as HD. If the max pixels per + /// frame is higher than max_sd_pixels, but no higher + /// than max_hd_pixels, i.e. (max_sd_pixels, + /// max_hd_pixels], it is HD. + /// @param max_uhd1_pixels: The threshold to determine whether a video track + /// should be considered as UHD1. If the max pixels + /// per frame is higher than max_hd_pixels, but no + /// higher than max_uhd1_pixels, i.e. (max_hd_pixels, + /// max_uhd1_pixels], it is UHD1. Otherwise it is + /// UHD2. + /// @param stream_info Encrypted stream info. + /// @return the stream label associated with `stream_info`. Can be "AUDIO", + /// "SD", "HD", "UHD1" or "UHD2". + static std::string DefaultStreamLabelFunction( + int max_sd_pixels, + int max_hd_pixels, + int max_uhd1_pixels, + const EncryptedStreamAttributes& stream_attributes); + const int kDefaultMaxSdPixels = 768 * 576; + const int kDefaultMaxHdPixels = 1920 * 1080; + const int kDefaultMaxUhd1Pixels = 4096 * 2160; + /// Stream label function assigns a stream label to the stream to be + /// encrypted. Stream label is used to associate KeyPair with streams. Streams + /// with the same stream label always uses the same keyPair; Streams with + /// different stream label could use the same or different KeyPairs. + std::function + stream_label_func = + std::bind(&EncryptionParams::DefaultStreamLabelFunction, + kDefaultMaxSdPixels, + kDefaultMaxHdPixels, + kDefaultMaxUhd1Pixels, + std::placeholders::_1); +}; + +/// Widevine decryption parameters. +struct WidevineDecryptionParams { + /// Widevine license / key server URL. + std::string key_server_url; + /// Signer credential for Widevine license / key server. + WidevineSigner signer; +}; + +/// Raw key decryption parameters, i.e. with key parameters provided. +struct RawKeyDecryptionParams { + using StreamLabel = std::string; + struct KeyPair { + std::string key_id; + std::string key; + }; + /// Defines the KeyPair for the streams. An empty `StreamLabel` indicates the + /// default `KeyPair`, which applies to all the `StreamLabels` not present in + /// `key_map`. + std::map key_map; +}; + +/// Decryption parameters. +struct DecryptionParams { + /// Specifies the key provider, which determines which key provider is used + /// and which encryption params is valid. 'kNone' means not to decrypt the + /// streams. + KeyProvider key_provider = KeyProvider::kNone; + // Only one of the two fields is valid. + WidevineDecryptionParams widevine; + RawKeyDecryptionParams raw_key; +}; + +/// Packaging parameters. +struct PackagingParams { + /// Specify temporary directory for intermediate temporary files. + std::string temp_dir; + /// MP4 (ISO-BMFF) output related parameters. + Mp4OutputParams mp4_output_params; + /// Chunking (segmentation) related parameters. + ChunkingParams chunking_params; + + /// Manifest generation related parameters. Right now only one of + /// `output_media_info`, `mpd_params` and `hls_params` should be set. Create a + /// human readable format of MediaInfo. The output file name will be the name + /// specified by output flag, suffixed with `.media_info`. + bool output_media_info = false; + /// DASH MPD related parameters. + MpdParams mpd_params; + /// HLS related parameters. + HlsParams hls_params; + + /// Encryption and Decryption Parameters. + EncryptionParams encryption_params; + DecryptionParams decryption_params; +}; + +/// Defines a single input/output stream. +struct StreamDescriptor { + /// Input/source media file path or network stream URL. Required. + std::string input; + // TODO(kqyang): Add support for feeding data through read func. + // std::function read_func; + + /// Stream selector, can be `audio`, `video`, `text` or a zero based stream + /// index. Required. + std::string stream_selector; + + /// Specifies output file path or init segment path (if segment template is + /// specified). Can be empty for self initialization media segments. + std::string output; + /// Specifies segment template. Can be empty. + std::string segment_template; + // TODO: Add support for writing data through write func. + // std::function write_func; + + /// Optional value which specifies output container format, e.g. "mp4". If not + /// specified, will detect from output / segment template name. + std::string output_format; + /// If set to true, the stream will not be encrypted. This is useful, e.g. to + /// encrypt only video streams. + bool skip_encryption = false; + /// If set to a non-zero value, will generate a trick play / trick mode + /// stream with frames sampled from the key frames in the original stream. + /// `trick_play_factor` defines the sampling rate. + uint32_t trick_play_factor = 0; + /// Optional user-specified content bit rate for the stream, in bits/sec. + /// If specified, this value is propagated to the `$Bandwidth$` template + /// parameter for segment names. If not specified, its value may be estimated. + uint32_t bandwidth = 0; + /// Optional value which contains a user-specified language tag. If specified, + /// this value overrides any language metadata in the input stream. + std::string language; + /// Required for audio when outputting HLS. It defines the name of the output + /// stream, which is not necessarily the same as output. This is used as the + /// `NAME` attribute for EXT-X-MEDIA. + std::string hls_name; + /// Required for audio when outputting HLS. It defines the group ID for the + /// output stream. This is used as the GROUP-ID attribute for EXT-X-MEDIA. + std::string hls_group_id; + /// Required for HLS output. It defines the name of the playlist for the + /// stream. Usually ends with `.m3u8`. + std::string hls_playlist_name; +}; + +class ShakaPackager { + public: + ShakaPackager(); + ~ShakaPackager(); + + /// Initialize packaging pipeline. + /// @param packaging_params contains the packaging parameters. + /// @param stream_descriptors a list of stream descriptors. + /// @return OK on success, an appropriate error code on failure. + media::Status Initialize( + const PackagingParams& packaging_params, + const std::vector& stream_descriptors); + + /// Run the pipeline to completion (or failed / been cancelled). Note + /// that it blocks until completion. + /// @return OK on success, an appropriate error code on failure. + media::Status Run(); + + /// Cancel packaging. Note that it has to be called from another thread. + void Cancel(); + + private: + ShakaPackager(const ShakaPackager&) = delete; + ShakaPackager& operator=(const ShakaPackager&) = delete; + + struct PackagerInternal; + std::unique_ptr internal_; +}; + +} // namespace shaka + +#endif // PACKAGER_PACKAGER_H_