| // 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 "cobalt/media/sandbox/media_source_demuxer.h" |
| |
| #include <algorithm> |
| #include <memory> |
| #include <string> |
| |
| #include "base/bind.h" |
| #include "base/callback.h" |
| #include "base/files/file_util.h" |
| #include "base/logging.h" |
| #include "base/message_loop/message_loop.h" |
| #include "base/sys_byteorder.h" |
| #include "cobalt/media/base/bind_to_current_loop.h" |
| #include "cobalt/media/base/demuxer.h" |
| #include "cobalt/media/base/pipeline_status.h" |
| #include "cobalt/media/filters/chunk_demuxer.h" |
| #include "starboard/memory.h" |
| |
| namespace cobalt { |
| namespace media { |
| namespace sandbox { |
| |
| namespace { |
| |
| using base::Bind; |
| using ::media::BindToCurrentLoop; |
| using ::media::DecoderBuffer; |
| using ::media::DemuxerStream; |
| using ::media::ChunkDemuxer; |
| |
| typedef base::Callback<void(::media::DecoderBuffer*)> AppendBufferCB; |
| |
| const char kSourceId[] = "id"; |
| |
| // Stub log function. |
| void Log(const std::string& message) {} |
| |
| // Stub need key callback. |
| void NeedKeyCB(const std::string& type, std::unique_ptr<uint8[]> init_data, |
| int init_data_size) { |
| NOTREACHED(); |
| } |
| |
| bool IsMP4(const std::vector<uint8>& content) { |
| return content.size() >= 8 && memcmp(&content[4], "ftyp", 4) == 0; |
| } |
| |
| std::vector<std::string> MakeStringVector(const char* string) { |
| std::vector<std::string> result; |
| result.push_back(string); |
| return result; |
| } |
| |
| bool AddSourceBuffer(bool is_mp4, ChunkDemuxer* demuxer) { |
| const char kMP4Mime[] = "video/mp4"; |
| const char kAVCCodecs[] = "avc1.640028"; |
| const char kWebMMime[] = "video/webm"; |
| const char kVp9Codecs[] = "vp9"; |
| |
| std::vector<std::string> codecs = |
| MakeStringVector(is_mp4 ? kAVCCodecs : kVp9Codecs); |
| ChunkDemuxer::Status status = |
| demuxer->AddId(kSourceId, is_mp4 ? kMP4Mime : kWebMMime, codecs); |
| CHECK_EQ(status, ChunkDemuxer::kOk); |
| return status == ChunkDemuxer::kOk; |
| } |
| |
| // Helper class to load an adaptive video file and call |append_buffer_cb| for |
| // every frame. |
| class Loader : public ::media::DemuxerHost { |
| public: |
| Loader(const std::vector<uint8>& content, |
| const AppendBufferCB& append_buffer_cb) |
| : valid_(true), |
| ended_(false), |
| append_buffer_cb_(append_buffer_cb), |
| content_(content), |
| offset_(0), |
| read_in_progress_(false) { |
| DCHECK(!append_buffer_cb.is_null()); |
| |
| demuxer_ = new ChunkDemuxer( |
| BindToCurrentLoop(Bind(&Loader::OnDemuxerOpen, base::Unretained(this), |
| IsMP4(content))), |
| Bind(NeedKeyCB), Bind(Log)); |
| demuxer_->Initialize( |
| this, ::media::BindToCurrentLoop( |
| Bind(&Loader::OnDemuxerStatus, base::Unretained(this)))); |
| while (valid_ && !ended_) { |
| base::RunLoop().RunUntilIdle(); |
| } |
| } |
| |
| bool valid() const { return valid_; } |
| const ::media::VideoDecoderConfig& config() const { return config_; } |
| |
| private: |
| void SetTotalBytes(int64 total_bytes) override { |
| } |
| void AddBufferedByteRange(int64 start, int64 end) override { |
| } |
| void AddBufferedTimeRange(base::TimeDelta start, |
| base::TimeDelta end) override { |
| } |
| |
| void SetDuration(base::TimeDelta duration) override { |
| } |
| void OnDemuxerError(::media::PipelineStatus error) override { |
| valid_ = false; |
| } |
| void OnDemuxerOpen(bool is_mp4) { |
| valid_ &= AddSourceBuffer(is_mp4, demuxer_); |
| if (!valid_) { |
| return; |
| } |
| ConsumeContent(); |
| if (!demuxer_->GetStream(DemuxerStream::VIDEO)) { |
| valid_ = false; |
| return; |
| } |
| while (valid_ && !ended_) { |
| ConsumeContent(); |
| ProduceBuffer(); |
| base::RunLoop().RunUntilIdle(); |
| } |
| } |
| void OnDemuxerStatus(::media::PipelineStatus status) { |
| if (status == ::media::PIPELINE_OK) { |
| config_.CopyFrom( |
| demuxer_->GetStream(DemuxerStream::VIDEO)->video_decoder_config()); |
| } else { |
| valid_ = false; |
| } |
| } |
| void ConsumeContent() { |
| const float kLowWaterMarkInSeconds = 1.f; |
| const size_t kMaxBytesToAppend = 5 * 1024 * 1024; |
| |
| ::media::Ranges<base::TimeDelta> ranges = |
| demuxer_->GetBufferedRanges(kSourceId); |
| if (offset_ < content_.size()) { |
| base::TimeDelta range_end; |
| if (ranges.size() != 0) { |
| range_end = ranges.end(ranges.size() - 1); |
| } |
| if ((range_end - timestamp_of_last_buffer_).InSecondsF() > |
| kLowWaterMarkInSeconds) { |
| return; |
| } |
| size_t bytes_to_append = |
| std::min(kMaxBytesToAppend, content_.size() - offset_); |
| demuxer_->AppendData(kSourceId, &content_[0] + offset_, bytes_to_append); |
| offset_ += bytes_to_append; |
| } |
| if (offset_ == content_.size()) { |
| demuxer_->EndOfStream(::media::PIPELINE_OK); |
| ++offset_; |
| } |
| } |
| void ProduceBuffer() { |
| if (!read_in_progress_) { |
| read_in_progress_ = true; |
| demuxer_->GetStream(DemuxerStream::VIDEO) |
| ->Read(Bind(&Loader::OnDemuxerRead, base::Unretained(this))); |
| } |
| } |
| void OnDemuxerRead(DemuxerStream::Status status, |
| const scoped_refptr<DecoderBuffer>& buffer) { |
| // This should only happen during seeking which won't happen to this class. |
| DCHECK_NE(status, DemuxerStream::kAborted); |
| if (status == DemuxerStream::kConfigChanged) { |
| config_.CopyFrom( |
| demuxer_->GetStream(DemuxerStream::VIDEO)->video_decoder_config()); |
| ProduceBuffer(); |
| return; |
| } |
| DCHECK_EQ(status, DemuxerStream::kOk); |
| if (buffer->IsEndOfStream()) { |
| ended_ = true; |
| } else { |
| timestamp_of_last_buffer_ = buffer->GetTimestamp(); |
| append_buffer_cb_.Run(buffer); |
| } |
| read_in_progress_ = false; |
| } |
| |
| bool valid_; |
| bool ended_; |
| AppendBufferCB append_buffer_cb_; |
| const std::vector<uint8>& content_; |
| size_t offset_; |
| bool read_in_progress_; |
| base::TimeDelta timestamp_of_last_buffer_; |
| scoped_refptr<ChunkDemuxer> demuxer_; |
| ::media::VideoDecoderConfig config_; |
| }; |
| |
| // This is a very loose IVF parser for fuzzing purpose and it ignores any |
| // invalid structure and just retrieves the frame data. |
| // IVF format (all data is little endian): |
| // 32 bytes file header starts with DKIF |
| // (4 bytes frame size + 8 bytes timestamp + <size> bytes frame data)* |
| bool LoadIVF(const std::vector<uint8>& content, |
| const AppendBufferCB& append_buffer_cb) { |
| const size_t kIVFFileHeaderSize = 32; |
| const size_t kIVFFrameHeaderSize = 12; |
| if (content.size() < kIVFFileHeaderSize || |
| memcmp(&content[0], "DKIF", 4) != 0) { |
| return false; |
| } |
| size_t offset = kIVFFileHeaderSize; |
| while (offset + kIVFFrameHeaderSize < content.size()) { |
| uint32 frame_size = base::ByteSwapToLE32( |
| *reinterpret_cast<const uint32*>(&content[offset])); |
| offset += kIVFFrameHeaderSize; |
| if (offset + frame_size > content.size()) { |
| break; |
| } |
| append_buffer_cb.Run(::media::StreamParserBuffer::CopyFrom( |
| &content[offset], frame_size, false)); |
| offset += frame_size; |
| } |
| return true; |
| } |
| |
| } // namespace |
| |
| MediaSourceDemuxer::MediaSourceDemuxer(const std::vector<uint8>& content) |
| : valid_(true) { |
| // Try to load it as an ivf first. |
| if (LoadIVF(content, Bind(&MediaSourceDemuxer::AppendBuffer, |
| base::Unretained(this)))) { |
| config_.Initialize( |
| ::media::kCodecVP9, ::media::VP9PROFILE_MAIN, ::media::VideoFrame::YV12, |
| ::media::COLOR_SPACE_HD_REC709, math::Size(1, 1), math::Rect(1, 1), |
| math::Size(1, 1), NULL, 0, false, false); |
| valid_ = descs_.size() > 0; |
| return; |
| } |
| |
| Loader loader( |
| content, Bind(&MediaSourceDemuxer::AppendBuffer, base::Unretained(this))); |
| valid_ = loader.valid(); |
| if (valid_) { |
| config_.CopyFrom(loader.config()); |
| } else { |
| au_data_.clear(); |
| descs_.clear(); |
| } |
| } |
| |
| void MediaSourceDemuxer::AppendBuffer(::media::DecoderBuffer* buffer) { |
| AUDescriptor desc = {0}; |
| desc.is_keyframe = buffer->IsKeyframe(); |
| desc.offset = au_data_.size(); |
| desc.size = buffer->GetDataSize(); |
| desc.timestamp = buffer->GetTimestamp(); |
| |
| au_data_.insert(au_data_.end(), buffer->GetData(), |
| buffer->GetData() + buffer->GetDataSize()); |
| descs_.push_back(desc); |
| } |
| |
| const MediaSourceDemuxer::AUDescriptor& MediaSourceDemuxer::GetFrame( |
| size_t index) const { |
| DCHECK_LT(index, descs_.size()); |
| |
| return descs_[index]; |
| } |
| |
| } // namespace sandbox |
| } // namespace media |
| } // namespace cobalt |