| // Copyright 2016 The Cobalt Authors. All Rights Reserved. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| #include <algorithm> |
| #include <memory> |
| #include <sstream> |
| #include <string> |
| #include <vector> |
| |
| #include "base/bind.h" |
| #include "base/bind_helpers.h" |
| #include "base/compiler_specific.h" |
| #include "base/memory/ref_counted.h" |
| #include "base/message_loop/message_loop.h" |
| #include "base/path_service.h" |
| #include "base/threading/platform_thread.h" |
| #include "cobalt/base/wrap_main.h" |
| #include "cobalt/media/sandbox/format_guesstimator.h" |
| #include "cobalt/media/sandbox/media_sandbox.h" |
| #include "cobalt/media/sandbox/web_media_player_helper.h" |
| #include "cobalt/render_tree/image.h" |
| #include "starboard/common/file.h" |
| #include "starboard/event.h" |
| #include "starboard/log.h" |
| #include "starboard/system.h" |
| |
| namespace cobalt { |
| namespace media { |
| namespace sandbox { |
| namespace { |
| |
| using base::TimeDelta; |
| using render_tree::Image; |
| using starboard::ScopedFile; |
| |
| void PrintUsage(const char* executable_path_name) { |
| std::string executable_file_name = |
| base::FilePath(executable_path_name).BaseName().value(); |
| const char kExampleAdaptiveAudioPathName[] = |
| "cobalt/demos/media-element-demo/dash-audio.mp4"; |
| const char kExampleAdaptiveVideoPathName[] = |
| "cobalt/demos/media-element-demo/dash-video-1080p.mp4"; |
| const char kExampleProgressiveUrl[] = |
| "https://storage.googleapis.com/yt-cobalt-media-element-demo/" |
| "progressive.mp4"; |
| std::stringstream ss; |
| // Head |
| ss << "\n\n" |
| << "======================== " << executable_file_name |
| << " ========================\n"; |
| |
| // Basic usage |
| ss << "Usage: " << executable_file_name |
| << " [OPTIONS] <adaptive audio file path>\n" |
| << " or: " << executable_file_name |
| << " [OPTIONS] <adaptive video file path>\n" |
| << " or: " << executable_file_name |
| << " [OPTIONS] <adaptive audio file path> " |
| << " <adaptive video file path>\n" |
| << " or: " << executable_file_name |
| << " [OPTIONS] <progressive video path or url>\n" |
| << "Play adaptive audio/video or progressive video\n\n"; |
| |
| // Options |
| ss << "OPTIONS:\n" |
| << " --dump_video_data: Dump video data into .dmp files\n" |
| << " --use_stub_audio_decoder: Use stub audio decoder to play the video\n" |
| << " --use_stub_audio_sink: Use stub audio sink to play the video\n" |
| << " --use_stub_video_decoder: Use stub video decoder to play the video\n" |
| << "\n"; |
| |
| // Usage examples |
| ss << "For example:\n " << executable_file_name << " --dump_video_data " |
| << kExampleAdaptiveAudioPathName << "\n " << executable_file_name << " " |
| << kExampleAdaptiveVideoPathName << "\n " << executable_file_name << " " |
| << kExampleAdaptiveAudioPathName << " " << kExampleAdaptiveVideoPathName |
| << "\n " << executable_file_name << " " << kExampleProgressiveUrl |
| << "\n\n"; |
| SbLogRaw(ss.str().c_str()); |
| } |
| |
| void OnInitSegmentReceived(std::unique_ptr<MediaTracks> tracks) { |
| } |
| |
| class InitCobaltHelper { |
| public: |
| InitCobaltHelper(int argc, char* argv[]) { |
| cobalt::InitCobalt(argc, argv, NULL); |
| } |
| |
| private: |
| base::AtExitManager at_exit_manager_; |
| }; |
| |
| class Application { |
| public: |
| Application(int argc, char* argv[]) |
| : init_cobalt_helper_(argc, argv), |
| media_sandbox_(argc, argv, |
| base::FilePath(FILE_PATH_LITERAL( |
| "media_source_sandbox_trace.json"))) { |
| if (argc > 1) { |
| FormatGuesstimator guesstimator1(argv[argc - 1], |
| media_sandbox_.GetMediaModule()); |
| FormatGuesstimator guesstimator2(argv[argc - 2], |
| media_sandbox_.GetMediaModule()); |
| |
| if (!guesstimator1.is_valid()) { |
| SB_LOG(ERROR) << "Invalid path or url: " << argv[argc - 1]; |
| // Fall off to PrintUsage() and terminate. |
| } else if (guesstimator1.is_progressive()) { |
| InitializeProgressivePlayback(guesstimator1); |
| return; |
| } else if (!guesstimator2.is_adaptive()) { |
| InitializeAdaptivePlayback(guesstimator1); |
| return; |
| } else if (guesstimator1.is_audio() && guesstimator2.is_audio()) { |
| SB_LOG(ERROR) << "Failed to play because both " << argv[argc - 1] |
| << " and " << argv[argc - 2] |
| << " are audio streams, check usage for more details."; |
| // Fall off to PrintUsage() and terminate. |
| } else if (!guesstimator1.is_audio() && !guesstimator2.is_audio()) { |
| SB_LOG(ERROR) << "Failed to play because both " << argv[argc - 1] |
| << " and " << argv[argc - 2] |
| << " are video streams, check usage for more details."; |
| // Fall off to PrintUsage() and terminate. |
| } else if (guesstimator1.is_audio()) { |
| InitializeAdaptivePlayback(guesstimator1, guesstimator2); |
| return; |
| } else { |
| InitializeAdaptivePlayback(guesstimator2, guesstimator1); |
| return; |
| } |
| } |
| |
| PrintUsage(argv[0]); |
| SbSystemRequestStop(0); |
| } |
| ~Application() { media_sandbox_.RegisterFrameCB(MediaSandbox::FrameCB()); } |
| |
| private: |
| void InitializeAdaptivePlayback(const FormatGuesstimator& guesstimator) { |
| is_adaptive_playback_ = true; |
| |
| std::unique_ptr<ScopedFile>& file = |
| guesstimator.is_audio() ? audio_file_ : video_file_; |
| file.reset(new ScopedFile(guesstimator.adaptive_path().c_str(), |
| kSbFileOpenOnly | kSbFileRead)); |
| |
| if (!file->IsValid()) { |
| LOG(ERROR) << "Failed to open file: " << guesstimator.adaptive_path(); |
| SbSystemRequestStop(0); |
| return; |
| } |
| |
| player_helper_.reset(new WebMediaPlayerHelper( |
| media_sandbox_.GetMediaModule(), |
| base::Bind(&Application::OnChunkDemuxerOpened, base::Unretained(this)), |
| media_sandbox_.GetViewportSize())); |
| |
| // |chunk_demuxer_| will be set inside OnChunkDemuxerOpened() |
| // asynchronously during initialization of |player_helper_|. Wait until |
| // it is set before proceed. |
| while (!chunk_demuxer_) { |
| base::RunLoop().RunUntilIdle(); |
| } |
| |
| LOG(INFO) << "Playing " << guesstimator.adaptive_path(); |
| |
| std::string id = guesstimator.is_audio() ? kAudioId : kVideoId; |
| auto status = chunk_demuxer_->AddId(id, guesstimator.mime_type()); |
| CHECK_EQ(status, ChunkDemuxer::kOk); |
| |
| chunk_demuxer_->SetTracksWatcher(id, base::Bind(OnInitSegmentReceived)); |
| player_ = player_helper_->player(); |
| |
| media_sandbox_.RegisterFrameCB( |
| base::Bind(&Application::FrameCB, base::Unretained(this))); |
| |
| timer_event_id_ = |
| SbEventSchedule(Application::OnTimer, this, kSbTimeSecond / 10); |
| } |
| |
| void InitializeAdaptivePlayback( |
| const FormatGuesstimator& audio_guesstimator, |
| const FormatGuesstimator& video_guesstimator) { |
| is_adaptive_playback_ = true; |
| audio_file_.reset(new ScopedFile(audio_guesstimator.adaptive_path().c_str(), |
| kSbFileOpenOnly | kSbFileRead)); |
| video_file_.reset(new ScopedFile(video_guesstimator.adaptive_path().c_str(), |
| kSbFileOpenOnly | kSbFileRead)); |
| |
| if (!audio_file_->IsValid()) { |
| LOG(ERROR) << "Failed to open audio file: " |
| << audio_guesstimator.adaptive_path(); |
| SbSystemRequestStop(0); |
| return; |
| } |
| |
| if (!video_file_->IsValid()) { |
| LOG(ERROR) << "Failed to open video file: " |
| << video_guesstimator.adaptive_path(); |
| SbSystemRequestStop(0); |
| return; |
| } |
| |
| player_helper_.reset(new WebMediaPlayerHelper( |
| media_sandbox_.GetMediaModule(), |
| base::Bind(&Application::OnChunkDemuxerOpened, base::Unretained(this)), |
| media_sandbox_.GetViewportSize())); |
| |
| // |chunk_demuxer_| will be set inside OnChunkDemuxerOpened() |
| // asynchronously during initialization of |player_helper_|. Wait until |
| // it is set before proceed. |
| while (!chunk_demuxer_) { |
| base::RunLoop().RunUntilIdle(); |
| } |
| |
| LOG(INFO) << "Playing " << audio_guesstimator.adaptive_path() << " and " |
| << video_guesstimator.adaptive_path(); |
| |
| auto status = |
| chunk_demuxer_->AddId(kAudioId, audio_guesstimator.mime_type()); |
| CHECK_EQ(status, ChunkDemuxer::kOk); |
| |
| status = chunk_demuxer_->AddId(kVideoId, video_guesstimator.mime_type()); |
| CHECK_EQ(status, ChunkDemuxer::kOk); |
| |
| chunk_demuxer_->SetTracksWatcher(kAudioId, |
| base::Bind(OnInitSegmentReceived)); |
| chunk_demuxer_->SetTracksWatcher(kVideoId, |
| base::Bind(OnInitSegmentReceived)); |
| player_ = player_helper_->player(); |
| |
| media_sandbox_.RegisterFrameCB( |
| base::Bind(&Application::FrameCB, base::Unretained(this))); |
| |
| timer_event_id_ = |
| SbEventSchedule(Application::OnTimer, this, kSbTimeSecond / 10); |
| } |
| |
| void InitializeProgressivePlayback(const FormatGuesstimator& guesstimator) { |
| LOG(INFO) << "Playing " << guesstimator.progressive_url(); |
| |
| is_adaptive_playback_ = false; |
| |
| player_helper_.reset(new WebMediaPlayerHelper( |
| media_sandbox_.GetMediaModule(), media_sandbox_.GetFetcherFactory(), |
| guesstimator.progressive_url(), media_sandbox_.GetViewportSize())); |
| player_ = player_helper_->player(); |
| |
| media_sandbox_.RegisterFrameCB( |
| base::Bind(&Application::FrameCB, base::Unretained(this))); |
| |
| timer_event_id_ = |
| SbEventSchedule(Application::OnTimer, this, kSbTimeSecond / 10); |
| } |
| |
| static void OnTimer(void* context) { |
| Application* application = static_cast<Application*>(context); |
| DCHECK(application); |
| application->Tick(); |
| } |
| |
| void Tick() { |
| if (player_helper_->IsPlaybackFinished()) { |
| media_sandbox_.RegisterFrameCB(MediaSandbox::FrameCB()); |
| LOG(INFO) << "Playback finished."; |
| SbEventCancel(timer_event_id_); |
| SbSystemRequestStop(0); |
| return; |
| } |
| if (is_adaptive_playback_ && !eos_appended_) { |
| if (audio_file_) { |
| AppendData(kAudioId, audio_file_.get(), &audio_offset_); |
| } |
| if (video_file_) { |
| AppendData(kVideoId, video_file_.get(), &video_offset_); |
| } |
| bool audio_eos = !audio_file_ || audio_offset_ == audio_file_->GetSize(); |
| bool video_eos = !video_file_ || video_offset_ == video_file_->GetSize(); |
| if (audio_eos && video_eos) { |
| chunk_demuxer_->MarkEndOfStream(PIPELINE_OK); |
| eos_appended_ = true; |
| } |
| } |
| |
| base::RunLoop().RunUntilIdle(); |
| timer_event_id_ = |
| SbEventSchedule(Application::OnTimer, this, kSbTimeSecond / 10); |
| } |
| |
| void OnChunkDemuxerOpened(ChunkDemuxer* chunk_demuxer) { |
| CHECK(chunk_demuxer); |
| CHECK(!chunk_demuxer_); |
| |
| chunk_demuxer_ = chunk_demuxer; |
| } |
| |
| void AppendData(const std::string& id, ScopedFile* file, int64* offset) { |
| const float kLowWaterMarkInSeconds = 5.f; |
| const int64 kMaxBytesToAppend = 1024 * 1024; |
| std::vector<uint8_t> buffer(kMaxBytesToAppend); |
| |
| while (*offset < file->GetSize()) { |
| Ranges<TimeDelta> ranges = chunk_demuxer_->GetBufferedRanges(id); |
| float end_of_buffer = |
| ranges.size() == 0 ? 0.f : ranges.end(ranges.size() - 1).InSecondsF(); |
| if (end_of_buffer - player_->GetCurrentTime() > kLowWaterMarkInSeconds) { |
| break; |
| } |
| int64 bytes_to_append = |
| std::min(kMaxBytesToAppend, file->GetSize() - *offset); |
| |
| auto current_time = player_ ? player_->GetCurrentTime() : 0; |
| auto evicted = chunk_demuxer_->EvictCodedFrames( |
| id, base::TimeDelta::FromSecondsD(current_time), bytes_to_append); |
| SB_DCHECK(evicted); |
| |
| file->Read(reinterpret_cast<char*>(buffer.data()), bytes_to_append); |
| base::TimeDelta timestamp_offset; |
| auto appended = chunk_demuxer_->AppendData( |
| id, buffer.data(), bytes_to_append, base::TimeDelta(), |
| media::kInfiniteDuration, ×tamp_offset); |
| SB_DCHECK(appended); |
| |
| *offset += bytes_to_append; |
| } |
| } |
| |
| scoped_refptr<Image> FrameCB(const base::TimeDelta& time) { |
| SbDecodeTarget decode_target = player_helper_->GetCurrentDecodeTarget(); |
| |
| if (SbDecodeTargetIsValid(decode_target)) { |
| return media_sandbox_.resource_provider()->CreateImageFromSbDecodeTarget( |
| decode_target); |
| } |
| return NULL; |
| } |
| |
| const std::string kAudioId = "audio"; |
| const std::string kVideoId = "video"; |
| |
| bool is_adaptive_playback_; |
| InitCobaltHelper init_cobalt_helper_; |
| MediaSandbox media_sandbox_; |
| std::unique_ptr<WebMediaPlayerHelper> player_helper_; |
| ChunkDemuxer* chunk_demuxer_ = NULL; |
| WebMediaPlayer* player_ = NULL; |
| std::unique_ptr<ScopedFile> audio_file_; |
| std::unique_ptr<ScopedFile> video_file_; |
| int64 audio_offset_ = 0; |
| int64 video_offset_ = 0; |
| bool eos_appended_ = false; |
| SbEventId timer_event_id_ = kSbEventIdInvalid; |
| }; |
| |
| } // namespace |
| } // namespace sandbox |
| } // namespace media |
| } // namespace cobalt |
| |
| void SbEventHandle(const SbEvent* event) { |
| using cobalt::media::sandbox::Application; |
| |
| static Application* s_application; |
| |
| switch (event->type) { |
| case kSbEventTypeStart: { |
| SbEventStartData* data = static_cast<SbEventStartData*>(event->data); |
| DCHECK(!s_application); |
| if (data->argument_count == 1) { |
| cobalt::media::sandbox::PrintUsage(data->argument_values[0]); |
| SbSystemRequestStop(0); |
| return; |
| } |
| s_application = |
| new Application(data->argument_count, data->argument_values); |
| break; |
| } |
| case kSbEventTypeStop: { |
| delete s_application; |
| s_application = NULL; |
| break; |
| } |
| |
| default: { break; } |
| } |
| } |