| // Copyright 2016 Google Inc. 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 <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/memory/scoped_ptr.h" |
| #include "base/message_loop.h" |
| #include "base/path_service.h" |
| #include "base/threading/platform_thread.h" |
| #include "cobalt/base/wrap_main.h" |
| #include "cobalt/media/sandbox/media_sandbox.h" |
| #include "cobalt/media/sandbox/web_media_player_helper.h" |
| #include "cobalt/render_tree/image.h" |
| #if !defined(COBALT_MEDIA_SOURCE_2016) |
| #include "media/base/video_frame.h" |
| #endif // !defined(COBALT_MEDIA_SOURCE_2016) |
| #include "net/base/net_util.h" |
| #include "starboard/event.h" |
| #include "starboard/file.h" |
| #include "starboard/system.h" |
| |
| namespace cobalt { |
| namespace media { |
| namespace sandbox { |
| namespace { |
| |
| #if !defined(COBALT_MEDIA_SOURCE_2016) |
| using ::media::ChunkDemuxer; |
| using ::media::PIPELINE_OK; |
| using ::media::Ranges; |
| using ::media::VideoFrame; |
| using ::media::WebMediaPlayer; |
| #endif // !defined(COBALT_MEDIA_SOURCE_2016) |
| |
| using base::TimeDelta; |
| using render_tree::Image; |
| using starboard::ScopedFile; |
| |
| FilePath ResolvePath(const char* path) { |
| FilePath result(path); |
| if (!result.IsAbsolute()) { |
| FilePath content_path; |
| PathService::Get(base::DIR_TEST_DATA, &content_path); |
| CHECK(content_path.IsAbsolute()); |
| result = content_path.Append(result); |
| } |
| if (SbFileCanOpen(result.value().c_str(), kSbFileOpenOnly | kSbFileRead)) { |
| return result; |
| } |
| return FilePath(); |
| } |
| |
| GURL ResolveUrl(const char* url) { |
| GURL gurl(url); |
| if (gurl.is_valid()) { |
| return gurl; |
| } |
| |
| // Assume the input is a path. Try to figure out the path to this file and |
| // convert it to a URL. |
| FilePath path = ResolvePath(url); |
| if (path.empty()) { |
| return GURL(); |
| } |
| |
| return net::FilePathToFileURL(path); |
| } |
| |
| void PrintUsage(const char* executable_path_name) { |
| std::string executable_file_name = |
| 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"; |
| LOG(ERROR) << "\n\n\n" // Extra empty lines to separate from other messages |
| << "Usage: " << 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 video or progressive video\n\n" |
| << "For example:\n" |
| << executable_file_name << " " << kExampleAdaptiveAudioPathName |
| << " " << kExampleAdaptiveVideoPathName << "\n" |
| << executable_file_name << " " << kExampleProgressiveUrl << "\n\n"; |
| } |
| |
| #if defined(COBALT_MEDIA_SOURCE_2016) |
| |
| std::string MakeCodecParameter(const char* string) { return string; } |
| |
| void OnInitSegmentReceived(scoped_ptr<MediaTracks> tracks) { |
| UNREFERENCED_PARAMETER(tracks); |
| } |
| |
| #else // defined(COBALT_MEDIA_SOURCE_2016) |
| |
| std::vector<std::string> MakeCodecParameter(const char* string) { |
| std::vector<std::string> result; |
| result.push_back(string); |
| return result; |
| } |
| |
| #endif // defined(COBALT_MEDIA_SOURCE_2016) |
| |
| bool IsWebM(const std::string& path) { |
| return path.size() >= 5 && path.substr(path.size() - 5) == ".webm"; |
| } |
| |
| 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, |
| FilePath(FILE_PATH_LITERAL("media_source_sandbox_trace.json"))) { |
| if (argc > 2) { |
| FilePath audio_path = ResolvePath(argv[argc - 2]); |
| FilePath video_path = ResolvePath(argv[argc - 1]); |
| if (!audio_path.empty() && !video_path.empty()) { |
| is_adaptive_playback_ = true; |
| InitializeAdaptivePlayback(audio_path.value(), video_path.value()); |
| return; |
| } |
| } |
| |
| if (argc > 1) { |
| GURL video_url = ResolveUrl(argv[argc - 1]); |
| if (video_url.is_valid()) { |
| is_adaptive_playback_ = false; |
| InitializeProgressivePlayback(video_url); |
| return; |
| } |
| } |
| |
| PrintUsage(argv[0]); |
| SbSystemRequestStop(0); |
| } |
| |
| private: |
| void InitializeAdaptivePlayback(const std::string& audio_path, |
| const std::string& video_path) { |
| audio_file_.reset( |
| new ScopedFile(audio_path.c_str(), kSbFileOpenOnly | kSbFileRead)); |
| video_file_.reset( |
| new ScopedFile(video_path.c_str(), kSbFileOpenOnly | kSbFileRead)); |
| |
| if (!audio_file_->IsValid()) { |
| LOG(ERROR) << "Failed to open audio file: " << audio_path; |
| SbSystemRequestStop(0); |
| return; |
| } |
| |
| if (!video_file_->IsValid()) { |
| LOG(ERROR) << "Failed to open video file: " << video_path; |
| SbSystemRequestStop(0); |
| return; |
| } |
| |
| player_helper_.reset( |
| new WebMediaPlayerHelper(media_sandbox_.GetMediaModule(), |
| base::Bind(&Application::OnChunkDemuxerOpened, |
| base::Unretained(this)))); |
| |
| // |chunk_demuxer_| will be set inside OnChunkDemuxerOpened() |
| // asynchronously during initialization of |player_helper_|. Wait until |
| // it is set before proceed. |
| while (!chunk_demuxer_) { |
| MessageLoop::current()->RunUntilIdle(); |
| } |
| |
| LOG(INFO) << "Playing " << audio_path << " and " << video_path; |
| |
| AddSourceBuffers(IsWebM(audio_path), IsWebM(video_path)); |
| |
| #if defined(COBALT_MEDIA_SOURCE_2016) |
| chunk_demuxer_->SetTracksWatcher(kAudioId, |
| base::Bind(OnInitSegmentReceived)); |
| chunk_demuxer_->SetTracksWatcher(kVideoId, |
| base::Bind(OnInitSegmentReceived)); |
| #endif // defined(COBALT_MEDIA_SOURCE_2016) |
| 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 GURL& video_url) { |
| LOG(INFO) << "Playing " << video_url; |
| |
| player_helper_.reset(new WebMediaPlayerHelper( |
| media_sandbox_.GetMediaModule(), media_sandbox_.GetFetcherFactory(), |
| video_url)); |
| 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_) { |
| AppendData(kAudioId, audio_file_.get(), &audio_offset_); |
| AppendData(kVideoId, video_file_.get(), &video_offset_); |
| if (audio_offset_ == audio_file_->GetSize() && |
| video_offset_ == video_file_->GetSize()) { |
| #if defined(COBALT_MEDIA_SOURCE_2016) |
| chunk_demuxer_->MarkEndOfStream(PIPELINE_OK); |
| #else // defined(COBALT_MEDIA_SOURCE_2016) |
| chunk_demuxer_->EndOfStream(PIPELINE_OK); |
| #endif // defined(COBALT_MEDIA_SOURCE_2016) |
| eos_appended_ = true; |
| } |
| } |
| |
| MessageLoop::current()->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 AddSourceBuffers(bool is_audio_webm, bool is_video_webm) { |
| const char kAACMime[] = "audio/mp4"; |
| const char kAACCodecs[] = "mp4a.40.2"; |
| const char kAVCMime[] = "video/mp4"; |
| const char kAVCCodecs[] = "avc1.640028"; |
| |
| const char kOpusMime[] = "audio/webm"; |
| const char kOpusCodecs[] = "opus"; |
| const char kVp9Mime[] = "video/webm"; |
| const char kVp9Codecs[] = "vp9"; |
| |
| auto audio_codecs = |
| MakeCodecParameter(is_audio_webm ? kOpusCodecs : kAACCodecs); |
| auto status = chunk_demuxer_->AddId( |
| kAudioId, is_audio_webm ? kOpusMime : kAACMime, audio_codecs); |
| CHECK_EQ(status, ChunkDemuxer::kOk); |
| |
| auto video_codecs = |
| MakeCodecParameter(is_video_webm ? kVp9Codecs : kAVCCodecs); |
| status = chunk_demuxer_->AddId( |
| kVideoId, is_video_webm ? kVp9Mime : kAVCMime, video_codecs); |
| CHECK_EQ(status, ChunkDemuxer::kOk); |
| } |
| |
| void AppendData(const std::string& id, ScopedFile* file, int64* offset) { |
| const float kLowWaterMarkInSeconds = 5.f; |
| const int64 kMaxBytesToAppend = 1024 * 1024; |
| char 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); |
| file->Read(buffer, bytes_to_append); |
| #if defined(COBALT_MEDIA_SOURCE_2016) |
| base::TimeDelta timestamp_offset; |
| chunk_demuxer_->AppendData(id, reinterpret_cast<uint8*>(buffer), |
| bytes_to_append, base::TimeDelta(), |
| media::kInfiniteDuration, ×tamp_offset); |
| #else // defined(COBALT_MEDIA_SOURCE_2016) |
| chunk_demuxer_->AppendData(id, reinterpret_cast<uint8*>(buffer), |
| bytes_to_append); |
| #endif // defined(COBALT_MEDIA_SOURCE_2016) |
| |
| *offset += bytes_to_append; |
| } |
| } |
| |
| scoped_refptr<Image> FrameCB(const base::TimeDelta& time) { |
| UNREFERENCED_PARAMETER(time); |
| |
| #if SB_HAS(GRAPHICS) |
| SbDecodeTarget decode_target = player_helper_->GetCurrentDecodeTarget(); |
| |
| if (SbDecodeTargetIsValid(decode_target)) { |
| return media_sandbox_.resource_provider()->CreateImageFromSbDecodeTarget( |
| decode_target); |
| } |
| #if !defined(COBALT_MEDIA_SOURCE_2016) |
| scoped_refptr<VideoFrame> frame = player_helper_->GetCurrentFrame(); |
| return frame ? reinterpret_cast<Image*>(frame->texture_id()) : NULL; |
| #else // !defined(COBALT_MEDIA_SOURCE_2016) |
| return NULL; |
| #endif // !defined(COBALT_MEDIA_SOURCE_2016) |
| #else // SB_HAS(GRAPHICS) |
| return NULL; |
| #endif // SB_HAS(GRAPHICS) |
| } |
| |
| const std::string kAudioId = "audio"; |
| const std::string kVideoId = "video"; |
| |
| bool is_adaptive_playback_; |
| InitCobaltHelper init_cobalt_helper_; |
| MediaSandbox media_sandbox_; |
| scoped_ptr<WebMediaPlayerHelper> player_helper_; |
| ChunkDemuxer* chunk_demuxer_ = NULL; |
| WebMediaPlayer* player_ = NULL; |
| scoped_ptr<ScopedFile> audio_file_; |
| scoped_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); |
| s_application = |
| new Application(data->argument_count, data->argument_values); |
| break; |
| } |
| case kSbEventTypeStop: { |
| delete s_application; |
| s_application = NULL; |
| break; |
| } |
| |
| default: { break; } |
| } |
| } |