diff options
author | scherkus@chromium.org <scherkus@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2009-03-10 19:09:28 +0000 |
---|---|---|
committer | scherkus@chromium.org <scherkus@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2009-03-10 19:09:28 +0000 |
commit | d77ed9ef01dd6508ed6f153acbe71f95b9f38480 (patch) | |
tree | e4da61afea4578827b57e411e284462e500293ee | |
parent | 8d439a694371cfdd95c0c03ff452c9096d3cc7f3 (diff) | |
download | chromium_src-d77ed9ef01dd6508ed6f153acbe71f95b9f38480.zip chromium_src-d77ed9ef01dd6508ed6f153acbe71f95b9f38480.tar.gz chromium_src-d77ed9ef01dd6508ed6f153acbe71f95b9f38480.tar.bz2 |
Checking in media::FFmpegGlue, media::FFmpegDemuxer and tests.
Fixes build break by including FFmpeg dependency for media project in chrome.sln.
This is a second attempt at committing the following changelists:
http://codereview.chromium.org/39295
http://codereview.chromium.org/28165
TBR=darin
Review URL: http://codereview.chromium.org/42029
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@11356 0039d316-1c4b-4281-b951-d872f2087c98
-rw-r--r-- | chrome/chrome.sln | 3 | ||||
-rw-r--r-- | media/DEPS | 1 | ||||
-rw-r--r-- | media/base/mock_media_filters.h | 35 | ||||
-rw-r--r-- | media/base/pipeline.h | 7 | ||||
-rw-r--r-- | media/build/media.vcproj | 28 | ||||
-rw-r--r-- | media/build/media_unittests.vcproj | 12 | ||||
-rw-r--r-- | media/filters/ffmpeg_common.cc | 18 | ||||
-rw-r--r-- | media/filters/ffmpeg_common.h | 33 | ||||
-rw-r--r-- | media/filters/ffmpeg_demuxer.cc | 281 | ||||
-rw-r--r-- | media/filters/ffmpeg_demuxer.h | 127 | ||||
-rw-r--r-- | media/filters/ffmpeg_demuxer_unittest.cc | 360 | ||||
-rw-r--r-- | media/filters/ffmpeg_glue.cc | 162 | ||||
-rw-r--r-- | media/filters/ffmpeg_glue.h | 80 | ||||
-rw-r--r-- | media/filters/ffmpeg_glue_unittest.cc | 309 |
14 files changed, 1445 insertions, 11 deletions
diff --git a/chrome/chrome.sln b/chrome/chrome.sln index bd01935..090f209 100644 --- a/chrome/chrome.sln +++ b/chrome/chrome.sln @@ -585,6 +585,9 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "net_perftests", "..\net\bui EndProjectSection EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "media", "..\media\build\media.vcproj", "{6AE76406-B03B-11DD-94B1-80B556D89593}" + ProjectSection(ProjectDependencies) = postProject + {D7A94F58-576A-45D9-A45F-EB87C63ABBB0} = {D7A94F58-576A-45D9-A45F-EB87C63ABBB0} + EndProjectSection EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "media_unittests", "..\media\build\media_unittests.vcproj", "{C8C6183C-B03C-11DD-B471-DFD256D89593}" ProjectSection(ProjectDependencies) = postProject @@ -1,2 +1,3 @@ include_rules = [ + "+third_party/ffmpeg/include", ] diff --git a/media/base/mock_media_filters.h b/media/base/mock_media_filters.h index 079c681..0620eff 100644 --- a/media/base/mock_media_filters.h +++ b/media/base/mock_media_filters.h @@ -37,6 +37,7 @@ enum MockDataSourceBehavior { struct MockFilterConfig { MockFilterConfig() : data_source_behavior(MOCK_DATA_SOURCE_NORMAL_INIT), + data_source_value('!'), has_video(true), video_width(1280u), video_height(720u), @@ -52,6 +53,7 @@ struct MockFilterConfig { } MockDataSourceBehavior data_source_behavior; + char data_source_value; bool has_video; size_t video_width; size_t video_height; @@ -76,7 +78,16 @@ class MockDataSource : public DataSource { explicit MockDataSource(const MockFilterConfig* config) : config_(config), - position_(0) { + position_(0), + deleted_(NULL) { + } + + MockDataSource(const MockFilterConfig* config, bool* deleted) + : config_(config), + position_(0), + deleted_(deleted) { + EXPECT_TRUE(deleted); + EXPECT_FALSE(*deleted); } // Implementation of MediaFilter. @@ -121,7 +132,7 @@ class MockDataSource : public DataSource { if (size < read) { read = size; } - memset(data, 0, read); + memset(data, config_->data_source_value, read); return read; } @@ -131,8 +142,6 @@ class MockDataSource : public DataSource { } virtual bool SetPosition(int64 position) { - EXPECT_GE(position, 0u); - EXPECT_LE(position, config_->media_total_bytes); if (position < 0u || position > config_->media_total_bytes) { return false; } @@ -141,12 +150,22 @@ class MockDataSource : public DataSource { } virtual bool GetSize(int64* size_out) { - *size_out = config_->media_total_bytes; + if (config_->media_total_bytes >= 0) { + *size_out = config_->media_total_bytes; + return true; + } return false; } + // Simple position getter for unit testing. + int64 position() const { return position_; } + private: - virtual ~MockDataSource() {} + virtual ~MockDataSource() { + if (deleted_) { + *deleted_ = true; + } + } void TaskBehavior() { switch (config_->data_source_behavior) { @@ -166,6 +185,10 @@ class MockDataSource : public DataSource { int64 position_; MediaFormat media_format_; + // Set to true inside the destructor. Used in FFmpegGlue unit tests for + // testing proper reference counting. + bool* deleted_; + DISALLOW_COPY_AND_ASSIGN(MockDataSource); }; diff --git a/media/base/pipeline.h b/media/base/pipeline.h index da3dfc4..58883cc 100644 --- a/media/base/pipeline.h +++ b/media/base/pipeline.h @@ -31,7 +31,12 @@ enum PipelineError { PIPELINE_ERROR_REQUIRED_FILTER_MISSING, PIPELINE_ERROR_OUT_OF_MEMORY, PIPELINE_ERROR_COULD_NOT_RENDER, - PIPELINE_ERROR_READ + PIPELINE_ERROR_READ, + + // Demuxer related errors. + DEMUXER_ERROR_COULD_NOT_OPEN, + DEMUXER_ERROR_COULD_NOT_PARSE, + DEMUXER_ERROR_NO_SUPPORTED_STREAMS, }; // Base class for Pipeline class which allows for read-only access to members. diff --git a/media/build/media.vcproj b/media/build/media.vcproj index d0b081f..37b7977 100644 --- a/media/build/media.vcproj +++ b/media/build/media.vcproj @@ -18,7 +18,7 @@ <Configuration Name="Debug|Win32" ConfigurationType="4" - InheritedPropertySheets="$(SolutionDir)..\build\debug.vsprops" + InheritedPropertySheets="$(SolutionDir)..\build\debug.vsprops;$(SolutionDir)..\third_party\ffmpeg\using_ffmpeg.vsprops" > <Tool Name="VCPreBuildEventTool" @@ -69,7 +69,7 @@ <Configuration Name="Release|Win32" ConfigurationType="4" - InheritedPropertySheets="$(SolutionDir)..\build\release.vsprops" + InheritedPropertySheets="$(SolutionDir)..\build\release.vsprops;$(SolutionDir)..\third_party\ffmpeg\using_ffmpeg.vsprops" > <Tool Name="VCPreBuildEventTool" @@ -213,6 +213,30 @@ > </File> <File + RelativePath="..\filters\ffmpeg_common.cc" + > + </File> + <File + RelativePath="..\filters\ffmpeg_common.h" + > + </File> + <File + RelativePath="..\filters\ffmpeg_demuxer.cc" + > + </File> + <File + RelativePath="..\filters\ffmpeg_demuxer.h" + > + </File> + <File + RelativePath="..\filters\ffmpeg_glue.cc" + > + </File> + <File + RelativePath="..\filters\ffmpeg_glue.h" + > + </File> + <File RelativePath="..\filters\file_data_source.cc" > </File> diff --git a/media/build/media_unittests.vcproj b/media/build/media_unittests.vcproj index db414e7..4e18337 100644 --- a/media/build/media_unittests.vcproj +++ b/media/build/media_unittests.vcproj @@ -18,7 +18,7 @@ <Configuration Name="Debug|Win32" ConfigurationType="1" - InheritedPropertySheets="$(SolutionDir)..\build\debug.vsprops;$(SolutionDir)..\testing\using_gtest.vsprops" + InheritedPropertySheets="$(SolutionDir)..\build\debug.vsprops;$(SolutionDir)..\testing\using_gtest.vsprops;$(SolutionDir)..\third_party\ffmpeg\using_ffmpeg.vsprops" > <Tool Name="VCPreBuildEventTool" @@ -79,7 +79,7 @@ <Configuration Name="Release|Win32" ConfigurationType="1" - InheritedPropertySheets="$(SolutionDir)..\build\release.vsprops;$(SolutionDir)..\testing\using_gtest.vsprops" + InheritedPropertySheets="$(SolutionDir)..\build\release.vsprops;$(SolutionDir)..\testing\using_gtest.vsprops;$(SolutionDir)..\third_party\ffmpeg\using_ffmpeg.vsprops" > <Tool Name="VCPreBuildEventTool" @@ -192,6 +192,14 @@ Name="filters" > <File + RelativePath="..\filters\ffmpeg_demuxer_unittest.cc" + > + </File> + <File + RelativePath="..\filters\ffmpeg_glue_unittest.cc" + > + </File> + <File RelativePath="..\filters\file_data_source_unittest.cc" > </File> diff --git a/media/filters/ffmpeg_common.cc b/media/filters/ffmpeg_common.cc new file mode 100644 index 0000000..80ace45 --- /dev/null +++ b/media/filters/ffmpeg_common.cc @@ -0,0 +1,18 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "media/filters/ffmpeg_common.h" + +namespace media { + +const char kFFmpegCodecID[] = "FFmpegCodecID"; + +namespace mime_type { + +const char kFFmpegAudio[] = "audio/x-ffmpeg"; +const char kFFmpegVideo[] = "video/x-ffmpeg"; + +} // namespace mime_type + +} // namespace media diff --git a/media/filters/ffmpeg_common.h b/media/filters/ffmpeg_common.h new file mode 100644 index 0000000..baddbb1 --- /dev/null +++ b/media/filters/ffmpeg_common.h @@ -0,0 +1,33 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Used for FFmpeg error codes. +#include <cerrno> + +#include "base/compiler_specific.h" + +// Include FFmpeg header files. +extern "C" { +// Temporarily disable possible loss of data warning. +// TODO(scherkus): fix and upstream the compiler warnings. +MSVC_PUSH_DISABLE_WARNING(4244); +#include "third_party/ffmpeg/include/libavcodec/avcodec.h" +#include "third_party/ffmpeg/include/libavformat/avformat.h" +MSVC_POP_WARNING(); +} // extern "C" + +namespace media { + +// MediaFormat key identifying the CodecID. +extern const char kFFmpegCodecID[]; + +// FFmpeg MIME types. +namespace mime_type { + +extern const char kFFmpegAudio[]; +extern const char kFFmpegVideo[]; + +} // namespace mime_type + +} // namespace media diff --git a/media/filters/ffmpeg_demuxer.cc b/media/filters/ffmpeg_demuxer.cc new file mode 100644 index 0000000..4344147 --- /dev/null +++ b/media/filters/ffmpeg_demuxer.cc @@ -0,0 +1,281 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "base/string_util.h" +#include "base/time.h" +#include "media/base/filter_host.h" +#include "media/filters/ffmpeg_common.h" +#include "media/filters/ffmpeg_demuxer.h" +#include "media/filters/ffmpeg_glue.h" + +namespace media { + +// +// AVPacketBuffer +// +class AVPacketBuffer : public Buffer { + public: + AVPacketBuffer(AVPacket* packet, const base::TimeDelta& timestamp, + const base::TimeDelta& duration) + : packet_(packet) { + DCHECK(packet); + SetTimestamp(timestamp); + SetDuration(duration); + } + + virtual ~AVPacketBuffer() { + av_free_packet(packet_.get()); + } + + // Buffer implementation. + virtual const char* GetData() const { + return reinterpret_cast<const char*>(packet_->data); + } + + virtual size_t GetDataSize() const { + return static_cast<size_t>(packet_->size); + } + + private: + scoped_ptr<AVPacket> packet_; + + DISALLOW_COPY_AND_ASSIGN(AVPacketBuffer); +}; + + +// +// FFmpegDemuxerStream +// +FFmpegDemuxerStream::FFmpegDemuxerStream(FFmpegDemuxer* demuxer, + const AVStream& stream) + : demuxer_(demuxer) { + DCHECK(demuxer_); + + // Determine our media format. + switch (stream.codec->codec_type) { + case CODEC_TYPE_AUDIO: + media_format_.SetAsString(MediaFormat::kMimeType, + mime_type::kFFmpegAudio); + media_format_.SetAsInteger(MediaFormat::kChannels, + stream.codec->channels); + media_format_.SetAsInteger(MediaFormat::kSampleRate, + stream.codec->sample_rate); + break; + case CODEC_TYPE_VIDEO: + media_format_.SetAsString(MediaFormat::kMimeType, + mime_type::kFFmpegVideo); + media_format_.SetAsInteger(MediaFormat::kHeight, + stream.codec->height); + media_format_.SetAsInteger(MediaFormat::kWidth, + stream.codec->width); + break; + default: + NOTREACHED(); + break; + } + int codec_id = static_cast<int>(stream.codec->codec_id); + media_format_.SetAsInteger(kFFmpegCodecID, codec_id); + + // Calculate the time base and duration in microseconds. + int64 time_base_us = static_cast<int64>(av_q2d(stream.time_base) * + base::Time::kMicrosecondsPerSecond); + int64 duration_us = static_cast<int64>(time_base_us * stream.duration); + time_base_ = base::TimeDelta::FromMicroseconds(time_base_us); + duration_ = base::TimeDelta::FromMicroseconds(duration_us); +} + +FFmpegDemuxerStream::~FFmpegDemuxerStream() { + // Since |input_queue_| and |output_queue_| use scoped_refptr everything + // should get released. +} + +bool FFmpegDemuxerStream::HasPendingReads() { + AutoLock auto_lock(lock_); + return !output_queue_.empty(); +} + +void FFmpegDemuxerStream::EnqueuePacket(AVPacket* packet) { + base::TimeDelta timestamp = time_base_ * packet->pts; + base::TimeDelta duration = time_base_ * packet->duration; + Buffer* buffer = new AVPacketBuffer(packet, timestamp, duration); + DCHECK(buffer); + { + AutoLock auto_lock(lock_); + input_queue_.push_back(buffer); + } + FulfillPendingReads(); +} + +const MediaFormat* FFmpegDemuxerStream::GetMediaFormat() { + return &media_format_; +} + +void FFmpegDemuxerStream::Read(Assignable<Buffer>* buffer) { + DCHECK(buffer); + { + AutoLock auto_lock(lock_); + output_queue_.push_back(scoped_refptr< Assignable<Buffer> >(buffer)); + } + if (FulfillPendingReads()) { + demuxer_->ScheduleDemux(); + } +} + +bool FFmpegDemuxerStream::FulfillPendingReads() { + bool pending_reads = false; + while (true) { + scoped_refptr<Buffer> buffer_in; + scoped_refptr< Assignable<Buffer> > buffer_out; + { + AutoLock auto_lock(lock_); + pending_reads = !output_queue_.empty(); + if (input_queue_.empty() || output_queue_.empty()) { + break; + } + buffer_in = input_queue_.front(); + buffer_out = output_queue_.front(); + input_queue_.pop_front(); + output_queue_.pop_front(); + } + buffer_out->SetBuffer(buffer_in); + buffer_out->OnAssignment(); + } + return pending_reads; +} + + +// +// FFmpegDemuxer +// +FFmpegDemuxer::FFmpegDemuxer() + : demuxing_(false), + format_context_(NULL) { +} + +FFmpegDemuxer::~FFmpegDemuxer() { + if (format_context_) { + av_free(format_context_); + } + while (!streams_.empty()) { + delete streams_.back(); + streams_.pop_back(); + } +} + +void FFmpegDemuxer::ScheduleDemux() { + if (!demuxing_) { + demuxing_ = true; + host_->PostTask(NewRunnableMethod(this, &FFmpegDemuxer::Demux)); + } +} + +void FFmpegDemuxer::Stop() { + // TODO(scherkus): implement Stop(). + NOTIMPLEMENTED(); +} + +bool FFmpegDemuxer::Initialize(DataSource* data_source) { + // In order to get FFmpeg to use |data_source| for file IO we must transfer + // ownership via FFmpegGlue. We'll add |data_source| to FFmpegGlue and pass + // the resulting key to FFmpeg. FFmpeg will pass the key to FFmpegGlue which + // will take care of attaching |data_source| to an FFmpeg context. After + // we finish initializing the FFmpeg context we can remove |data_source| from + // FFmpegGlue. + // + // Refer to media/filters/ffmpeg_glue.h for details. + + // Add our data source and get our unique key. + std::string key = FFmpegGlue::get()->AddDataSource(data_source); + + // Open FFmpeg AVFormatContext. + DCHECK(!format_context_); + int result = av_open_input_file(&format_context_, key.c_str(), NULL, 0, NULL); + + // Remove our data source. + FFmpegGlue::get()->RemoveDataSource(data_source); + + if (result < 0) { + host_->Error(DEMUXER_ERROR_COULD_NOT_OPEN); + return false; + } + + // Fully initialize AVFormatContext by parsing the stream a little. + result = av_find_stream_info(format_context_); + if (result < 0) { + host_->Error(DEMUXER_ERROR_COULD_NOT_PARSE); + return false; + } + + // Create demuxer streams for all supported streams. + base::TimeDelta max_duration; + for (size_t i = 0; i < format_context_->nb_streams; ++i) { + CodecType codec_type = format_context_->streams[i]->codec->codec_type; + if (codec_type == CODEC_TYPE_AUDIO || codec_type == CODEC_TYPE_VIDEO) { + AVStream* stream = format_context_->streams[i]; + FFmpegDemuxerStream* demuxer_stream + = new FFmpegDemuxerStream(this, *stream); + DCHECK(demuxer_stream); + streams_.push_back(demuxer_stream); + max_duration = std::max(max_duration, demuxer_stream->duration()); + } + } + if (streams_.empty()) { + host_->Error(DEMUXER_ERROR_NO_SUPPORTED_STREAMS); + return false; + } + + // We have at least one supported stream, set the duration and notify we're + // done initializing. + host_->SetDuration(max_duration); + host_->InitializationComplete(); + return true; +} + +size_t FFmpegDemuxer::GetNumberOfStreams() { + return streams_.size(); +} + +DemuxerStream* FFmpegDemuxer::GetStream(int stream) { + DCHECK(stream >= 0); + DCHECK(stream < static_cast<int>(streams_.size())); + return streams_[stream]; +} + +void FFmpegDemuxer::Demux() { + DCHECK(demuxing_); + + // Loop until we've satisfied every stream. + while (StreamsHavePendingReads()) { + // Allocate and read an AVPacket from the media. + scoped_ptr<AVPacket> packet(new AVPacket()); + int result = av_read_frame(format_context_, packet.get()); + if (result < 0) { + // TODO(scherkus): handle end of stream by marking Buffer with the end of + // stream flag. + NOTIMPLEMENTED(); + break; + } + + // Queue the packet with the appropriate stream. + DCHECK(packet->stream_index >= 0); + DCHECK(packet->stream_index < static_cast<int>(streams_.size())); + FFmpegDemuxerStream* demuxer_stream = streams_[packet->stream_index]; + demuxer_stream->EnqueuePacket(packet.release()); + } + + // Finished demuxing. + demuxing_ = false; +} + +bool FFmpegDemuxer::StreamsHavePendingReads() { + StreamVector::iterator iter; + for (iter = streams_.begin(); iter != streams_.end(); ++iter) { + if ((*iter)->HasPendingReads()) { + return true; + } + } + return false; +} + +} // namespace media diff --git a/media/filters/ffmpeg_demuxer.h b/media/filters/ffmpeg_demuxer.h new file mode 100644 index 0000000..895dd65 --- /dev/null +++ b/media/filters/ffmpeg_demuxer.h @@ -0,0 +1,127 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Implements the Demuxer interface using FFmpeg's libavformat. At this time +// will support demuxing any audio/video format thrown at it. The streams +// output mime types audio/x-ffmpeg and video/x-ffmpeg and include an integer +// key FFmpegCodecID which contains the CodecID enumeration value. The CodecIDs +// can be used to create and initialize the corresponding FFmpeg decoder. +// +// FFmpegDemuxer sets the duration of pipeline during initialization by using +// the duration of the longest audio/video stream. +// +// NOTE: since FFmpegDemuxer reads packets sequentially without seeking, media +// files with very large drift between audio/video streams may result in +// excessive memory consumption. + +#ifndef MEDIA_FILTERS_FFMPEG_DEMUXER_H_ +#define MEDIA_FILTERS_FFMPEG_DEMUXER_H_ + +#include <deque> +#include <vector> + +#include "base/lock.h" +#include "media/base/buffers.h" +#include "media/base/factory.h" +#include "media/base/filters.h" +#include "media/base/media_format.h" + +// FFmpeg forward declarations. +struct AVCodecContext; +struct AVBitStreamFilterContext; +struct AVFormatContext; +struct AVPacket; +struct AVStream; +enum CodecID; + +namespace media { + +class FFmpegDemuxer; + +class FFmpegDemuxerStream : public DemuxerStream { + public: + // Maintains a reference to |demuxer| and initializes itself using information + // inside |stream|. + FFmpegDemuxerStream(FFmpegDemuxer* demuxer, const AVStream& stream); + + virtual ~FFmpegDemuxerStream(); + + // Returns true is this stream has pending reads, false otherwise. + bool HasPendingReads(); + + // Enqueues and takes ownership over the given AVPacket. + void EnqueuePacket(AVPacket* packet); + + // Returns the duration of this stream. + base::TimeDelta duration() { return duration_; } + + // DemuxerStream implementation. + virtual const MediaFormat* GetMediaFormat(); + virtual void Read(Assignable<Buffer>* buffer); + + private: + // Returns true if there are still pending reads. + bool FulfillPendingReads(); + + FFmpegDemuxer* demuxer_; + MediaFormat media_format_; + base::TimeDelta time_base_; + base::TimeDelta duration_; + Lock lock_; + + typedef std::deque< scoped_refptr<Buffer> > InputQueue; + InputQueue input_queue_; + + typedef std::deque< scoped_refptr< Assignable<Buffer> > > OutputQueue; + OutputQueue output_queue_; + + DISALLOW_COPY_AND_ASSIGN(FFmpegDemuxerStream); +}; + +class FFmpegDemuxer : public Demuxer { + public: + // FilterFactory provider. + static FilterFactory* CreateFilterFactory() { + return new FilterFactoryImpl0<FFmpegDemuxer>(); + } + + // Called by FFmpegDemuxerStreams to schedule a Demux() task. + void ScheduleDemux(); + + // MediaFilter implementation. + virtual void Stop(); + + // Demuxer implementation. + virtual bool Initialize(DataSource* data_source); + virtual size_t GetNumberOfStreams(); + virtual DemuxerStream* GetStream(int stream_id); + + private: + // Only allow a factory to create this class. + friend class FilterFactoryImpl0<FFmpegDemuxer>; + FFmpegDemuxer(); + virtual ~FFmpegDemuxer(); + + // Demuxing task scheduled by streams. + void Demux(); + + // Returns true if any of the streams have pending reads. + bool StreamsHavePendingReads(); + + // Flag to prevent multiple Demux() tasks from being scheduled. + bool demuxing_; + + // FFmpeg context handle. + AVFormatContext* format_context_; + + // Vector of streams. + typedef std::vector<FFmpegDemuxerStream*> StreamVector; + StreamVector streams_; + + DISALLOW_COPY_AND_ASSIGN(FFmpegDemuxer); +}; + +} // namespace media + +#endif // MEDIA_FILTERS_FFMPEG_DEMUXER_H_ diff --git a/media/filters/ffmpeg_demuxer_unittest.cc b/media/filters/ffmpeg_demuxer_unittest.cc new file mode 100644 index 0000000..2c4ecbe --- /dev/null +++ b/media/filters/ffmpeg_demuxer_unittest.cc @@ -0,0 +1,360 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include <deque> + +#include "media/base/filter_host.h" +#include "media/base/filters.h" +#include "media/base/mock_filter_host.h" +#include "media/base/mock_media_filters.h" +#include "media/filters/ffmpeg_common.h" +#include "media/filters/ffmpeg_demuxer.h" +#include "testing/gtest/include/gtest/gtest.h" + +// FFmpeg mocks to remove dependency on having the DLLs present. +extern "C" { +static const size_t kMaxStreams = 3; +static AVFormatContext g_format; +static AVStream g_streams[kMaxStreams]; +static AVCodecContext g_audio_codec; +static AVCodecContext g_video_codec; +static AVCodecContext g_data_codec; +struct AVPacket g_packet; + +// FFmpeg return codes for various functions. +static int g_av_open_input_file = 0; +static int g_av_find_stream_info = 0; +static int g_av_read_frame = 0; + +// Counts the number of packets "allocated" by av_read_frame and "released" by +// av_free_packet. This should always be zero after everything is cleaned up. +static int g_oustanding_packets = 0; + +int av_open_input_file(AVFormatContext** format, const char* filename, + AVInputFormat* input_format, int buffer_size, + AVFormatParameters* parameters) { + EXPECT_FALSE(input_format) << "AVInputFormat should be NULL."; + EXPECT_FALSE(buffer_size) << "buffer_size should be 0."; + EXPECT_FALSE(parameters) << "AVFormatParameters should be NULL."; + if (g_av_open_input_file < 0) { + *format = NULL; + } else { + *format = &g_format; + } + return g_av_open_input_file; +} + +int av_find_stream_info(AVFormatContext* format) { + EXPECT_EQ(&g_format, format); + return g_av_find_stream_info; +} + +void av_free(void* ptr) { + EXPECT_EQ(&g_format, ptr); +} + +// Our packet destroying function. +void DestructPacket(AVPacket* packet) { + --g_oustanding_packets; +} + +int av_read_frame(AVFormatContext* format, AVPacket* packet) { + EXPECT_EQ(&g_format, format); + memcpy(packet, &g_packet, sizeof(g_packet)); + packet->destruct = &DestructPacket; + if (g_av_read_frame == 0) { + ++g_oustanding_packets; + } + return g_av_read_frame; +} + +} // extern "C" + +using namespace media; + +namespace { + +void InitializeFFmpegMocks() { + // Initialize function return codes. + g_av_open_input_file = 0; + g_av_find_stream_info = 0; + g_av_read_frame = 0; + + // Initialize AVFormatContext structure. + memset(&g_format, 0, sizeof(g_format)); + + // Initialize AVStream structures. + for (size_t i = 0; i < kMaxStreams; ++i) { + memset(&g_streams[i], 0, sizeof(g_streams[i])); + g_streams[i].time_base.den = 1 * base::Time::kMicrosecondsPerSecond; + g_streams[i].time_base.num = 1; + } + + // Initialize AVCodexContext structures. + memset(&g_audio_codec, 0, sizeof(g_audio_codec)); + g_audio_codec.codec_type = CODEC_TYPE_AUDIO; + g_audio_codec.codec_id = CODEC_ID_VORBIS; + g_audio_codec.channels = 2; + g_audio_codec.sample_rate = 44100; + + memset(&g_video_codec, 0, sizeof(g_video_codec)); + g_video_codec.codec_type = CODEC_TYPE_VIDEO; + g_video_codec.codec_id = CODEC_ID_THEORA; + g_video_codec.height = 720; + g_video_codec.width = 1280; + + memset(&g_data_codec, 0, sizeof(g_data_codec)); + g_data_codec.codec_type = CODEC_TYPE_DATA; + g_data_codec.codec_id = CODEC_ID_NONE; + + // Initialize AVPacket structure. + memset(&g_packet, 0, sizeof(g_packet)); +} + +// Simple implementation of Assignable<Buffer> that lets us poke at values. +class TestBuffer : public Assignable<Buffer> { + public: + TestBuffer() : assigned_(false) {} + virtual ~TestBuffer() {} + + // Assignable<Buffer> implementation. + virtual void SetBuffer(Buffer* buffer) { + buffer_ = buffer; + } + + void OnAssignment() { + EXPECT_FALSE(assigned_); + assigned_ = true; + } + + // Mock getters/setters. + Buffer* buffer() { return buffer_; } + bool assigned() { return assigned_; } + + private: + scoped_refptr<Buffer> buffer_; + bool assigned_; +}; + +} // namespace + +TEST(FFmpegDemuxerTest, InitializeFailure) { + InitializeFFmpegMocks(); + + // Get FFmpegDemuxer's filter factory. + scoped_refptr<FilterFactory> factory = FFmpegDemuxer::CreateFilterFactory(); + + // Should only accept application/octet-stream type. + MediaFormat media_format; + media_format.SetAsString(MediaFormat::kMimeType, "foo/x-bar"); + scoped_refptr<Demuxer> demuxer(factory->Create<Demuxer>(&media_format)); + ASSERT_FALSE(demuxer); + media_format.Clear(); + media_format.SetAsString(MediaFormat::kMimeType, + mime_type::kApplicationOctetStream); + demuxer = factory->Create<Demuxer>(&media_format); + ASSERT_TRUE(demuxer); + + // Prepare a filter host and data source for the demuxer. + MockPipeline pipeline; + scoped_ptr< MockFilterHost<Demuxer> > filter_host; + filter_host.reset(new MockFilterHost<Demuxer>(&pipeline, demuxer)); + MockFilterConfig config; + scoped_refptr<MockDataSource> data_source(new MockDataSource(&config)); + + // Simulate av_open_input_fail failing. + g_av_open_input_file = AVERROR_IO; + g_av_find_stream_info = 0; + EXPECT_FALSE(demuxer->Initialize(data_source)); + EXPECT_FALSE(filter_host->IsInitialized()); + EXPECT_EQ(DEMUXER_ERROR_COULD_NOT_OPEN, pipeline.GetError()); + + // Simulate av_find_stream_info failing. + g_av_open_input_file = 0; + g_av_find_stream_info = AVERROR_IO; + demuxer = factory->Create<Demuxer>(&media_format); + filter_host.reset(new MockFilterHost<Demuxer>(&pipeline, demuxer)); + EXPECT_FALSE(demuxer->Initialize(data_source)); + EXPECT_FALSE(filter_host->IsInitialized()); + EXPECT_EQ(DEMUXER_ERROR_COULD_NOT_PARSE, pipeline.GetError()); + + // Simulate media with no parseable streams. + InitializeFFmpegMocks(); + demuxer = factory->Create<Demuxer>(&media_format); + filter_host.reset(new MockFilterHost<Demuxer>(&pipeline, demuxer)); + EXPECT_FALSE(demuxer->Initialize(data_source)); + EXPECT_FALSE(filter_host->IsInitialized()); + EXPECT_EQ(DEMUXER_ERROR_NO_SUPPORTED_STREAMS, pipeline.GetError()); + + // Simulate media with a data stream but no audio or video streams. + g_format.nb_streams = 1; + g_format.streams[0] = &g_streams[0]; + g_streams[0].codec = &g_data_codec; + g_streams[0].duration = 10; + demuxer = factory->Create<Demuxer>(&media_format); + filter_host.reset(new MockFilterHost<Demuxer>(&pipeline, demuxer)); + EXPECT_FALSE(demuxer->Initialize(data_source)); + EXPECT_FALSE(filter_host->IsInitialized()); + EXPECT_EQ(DEMUXER_ERROR_NO_SUPPORTED_STREAMS, pipeline.GetError()); +} + +TEST(FFmpegDemuxerTest, InitializeStreams) { + // Simulate media with a data stream, a video stream and audio stream. + InitializeFFmpegMocks(); + g_format.nb_streams = 3; + g_format.streams[0] = &g_streams[0]; + g_format.streams[1] = &g_streams[1]; + g_format.streams[2] = &g_streams[2]; + g_streams[0].duration = 1000; + g_streams[0].codec = &g_data_codec; + g_streams[1].duration = 100; + g_streams[1].codec = &g_video_codec; + g_streams[2].duration = 10; + g_streams[2].codec = &g_audio_codec; + + // Create our pipeline. + MockPipeline pipeline; + + // Create our data source. + MockFilterConfig config; + scoped_refptr<MockDataSource> data_source = new MockDataSource(&config); + MockFilterHost<DataSource> filter_host_a(&pipeline, data_source); + EXPECT_TRUE(data_source->Initialize("foo")); + EXPECT_TRUE(filter_host_a.IsInitialized()); + + // Create our demuxer. + scoped_refptr<FilterFactory> factory = FFmpegDemuxer::CreateFilterFactory(); + scoped_refptr<Demuxer> demuxer + = factory->Create<Demuxer>(data_source->GetMediaFormat()); + EXPECT_TRUE(demuxer); + MockFilterHost<Demuxer> filter_host_b(&pipeline, demuxer); + EXPECT_TRUE(demuxer->Initialize(data_source)); + EXPECT_TRUE(filter_host_b.IsInitialized()); + EXPECT_EQ(PIPELINE_OK, pipeline.GetError()); + + // Since we ignore data streams, the duration should be equal to the video + // stream's duration. + EXPECT_EQ(g_streams[1].duration, pipeline.GetDuration().InMicroseconds()); + + // Verify that 2 out of 3 streams were created. + EXPECT_EQ(2, demuxer->GetNumberOfStreams()); + + // First stream should be video. + DemuxerStream* stream = demuxer->GetStream(0); + ASSERT_TRUE(stream); + const MediaFormat* stream_format = stream->GetMediaFormat(); + std::string mime_type; + int result; + EXPECT_TRUE(stream_format->GetAsString(MediaFormat::kMimeType, &mime_type)); + EXPECT_STREQ(mime_type::kFFmpegVideo, mime_type.c_str()); + EXPECT_TRUE(stream_format->GetAsInteger(kFFmpegCodecID, &result)); + EXPECT_EQ(CODEC_ID_THEORA, static_cast<CodecID>(result)); + EXPECT_TRUE(stream_format->GetAsInteger(MediaFormat::kHeight, &result)); + EXPECT_EQ(g_video_codec.height, result); + EXPECT_TRUE(stream_format->GetAsInteger(MediaFormat::kWidth, &result)); + EXPECT_EQ(g_video_codec.width, result); + + // Second stream should be audio. + stream = demuxer->GetStream(1); + ASSERT_TRUE(stream); + stream_format = stream->GetMediaFormat(); + EXPECT_TRUE(stream_format->GetAsString(MediaFormat::kMimeType, &mime_type)); + EXPECT_STREQ(mime_type::kFFmpegAudio, mime_type.c_str()); + EXPECT_TRUE(stream_format->GetAsInteger(kFFmpegCodecID, &result)); + EXPECT_EQ(CODEC_ID_VORBIS, static_cast<CodecID>(result)); + EXPECT_TRUE(stream_format->GetAsInteger(MediaFormat::kChannels, &result)); + EXPECT_EQ(g_audio_codec.channels, result); + EXPECT_TRUE(stream_format->GetAsInteger(MediaFormat::kSampleRate, &result)); + EXPECT_EQ(g_audio_codec.sample_rate, result); +} + +TEST(FFmpegDemuxerTest, Read) { + // Prepare some test data. + const int kAudio = 0; + const int kVideo = 1; + const size_t kDataSize = 4; + uint8 audio_data[kDataSize] = {0, 1, 2, 3}; + uint8 video_data[kDataSize] = {4, 5, 6, 7}; + + // Simulate media with a an audio stream and video stream. + InitializeFFmpegMocks(); + g_format.nb_streams = 2; + g_format.streams[kAudio] = &g_streams[kAudio]; + g_format.streams[kVideo] = &g_streams[kVideo]; + g_streams[kAudio].duration = 10; + g_streams[kAudio].codec = &g_audio_codec; + g_streams[kVideo].duration = 10; + g_streams[kVideo].codec = &g_video_codec; + + // Create our pipeline. + MockPipeline pipeline; + + // Create our data source. + MockFilterConfig config; + scoped_refptr<MockDataSource> data_source = new MockDataSource(&config); + MockFilterHost<DataSource> filter_host_a(&pipeline, data_source); + EXPECT_TRUE(data_source->Initialize("foo")); + EXPECT_TRUE(filter_host_a.IsInitialized()); + + // Create our demuxer. + scoped_refptr<FilterFactory> factory = FFmpegDemuxer::CreateFilterFactory(); + scoped_refptr<Demuxer> demuxer + = factory->Create<Demuxer>(data_source->GetMediaFormat()); + EXPECT_TRUE(demuxer); + MockFilterHost<Demuxer> filter_host_b(&pipeline, demuxer); + EXPECT_TRUE(demuxer->Initialize(data_source)); + EXPECT_TRUE(filter_host_b.IsInitialized()); + EXPECT_EQ(PIPELINE_OK, pipeline.GetError()); + + // Verify both streams were created. + EXPECT_EQ(2, demuxer->GetNumberOfStreams()); + + // Get our streams. + DemuxerStream* audio_stream = demuxer->GetStream(kAudio); + DemuxerStream* video_stream = demuxer->GetStream(kVideo); + ASSERT_TRUE(audio_stream); + ASSERT_TRUE(video_stream); + + // Prepare our test audio packet. + g_packet.stream_index = kAudio; + g_packet.data = audio_data; + g_packet.size = kDataSize; + + // Attempt a read from the audio stream and run the message loop until done. + scoped_refptr<TestBuffer> buffer(new TestBuffer()); + audio_stream->Read(buffer); + pipeline.RunAllTasks(); + EXPECT_TRUE(buffer->assigned()); + EXPECT_TRUE(buffer->buffer()); + EXPECT_EQ(audio_data, (uint8*)buffer->buffer()->GetData()); + EXPECT_EQ(kDataSize, buffer->buffer()->GetDataSize()); + + // Prepare our test video packet. + g_packet.stream_index = kVideo; + g_packet.data = video_data; + g_packet.size = kDataSize; + + // Attempt a read from the video stream and run the message loop until done. + buffer = new TestBuffer(); + video_stream->Read(buffer); + pipeline.RunAllTasks(); + EXPECT_TRUE(buffer->assigned()); + EXPECT_TRUE(buffer->buffer()); + EXPECT_EQ(video_data, (uint8*)buffer->buffer()->GetData()); + EXPECT_EQ(kDataSize, buffer->buffer()->GetDataSize()); + + // Simulate end of stream. + g_av_read_frame = AVERROR_IO; + + // Attempt a read from the audio stream and run the message loop until done. + buffer = new TestBuffer(); + audio_stream->Read(buffer); + pipeline.RunAllTasks(); + EXPECT_FALSE(buffer->assigned()); + EXPECT_FALSE(buffer->buffer()); + + // Manually release buffer, which should release any remaining AVPackets. + buffer = NULL; + EXPECT_EQ(0, g_oustanding_packets); +} diff --git a/media/filters/ffmpeg_glue.cc b/media/filters/ffmpeg_glue.cc new file mode 100644 index 0000000..0d3957b --- /dev/null +++ b/media/filters/ffmpeg_glue.cc @@ -0,0 +1,162 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "base/string_util.h" +#include "media/base/filters.h" +#include "media/filters/ffmpeg_common.h" +#include "media/filters/ffmpeg_glue.h" + +namespace { + +// FFmpeg protocol interface. +int OpenContext(URLContext* h, const char* filename, int flags) { + scoped_refptr<media::DataSource> data_source; + media::FFmpegGlue::get()->GetDataSource(filename, &data_source); + if (!data_source) + return AVERROR_IO; + + data_source->AddRef(); + h->priv_data = data_source; + h->flags = URL_RDONLY; + // TODO(scherkus): data source should be able to tell us if we're streaming. + h->is_streamed = FALSE; + return 0; +} + +int ReadContext(URLContext* h, unsigned char* buf, int size) { + media::DataSource* data_source = + reinterpret_cast<media::DataSource*>(h->priv_data); + int result = data_source->Read(buf, size); + if (result < 0) + result = AVERROR_IO; + return result; +} + +int WriteContext(URLContext* h, unsigned char* buf, int size) { + // We don't support writing. + return AVERROR_IO; +} + +offset_t SeekContext(URLContext* h, offset_t offset, int whence) { + media::DataSource* data_source = + reinterpret_cast<media::DataSource*>(h->priv_data); + offset_t new_offset = AVERROR_IO; + switch (whence) { + case SEEK_SET: + if (data_source->SetPosition(offset)) + data_source->GetPosition(&new_offset); + break; + + case SEEK_CUR: + int64 pos; + if (!data_source->GetPosition(&pos)) + break; + if (data_source->SetPosition(pos + offset)) + data_source->GetPosition(&new_offset); + break; + + case SEEK_END: + int64 size; + if (!data_source->GetSize(&size)) + break; + if (data_source->SetPosition(size + offset)) + data_source->GetPosition(&new_offset); + break; + + case AVSEEK_SIZE: + data_source->GetSize(&new_offset); + break; + + default: + NOTREACHED(); + } + if (new_offset < 0) + new_offset = AVERROR_IO; + return new_offset; +} + +int CloseContext(URLContext* h) { + media::DataSource* data_source = + reinterpret_cast<media::DataSource*>(h->priv_data); + data_source->Release(); + h->priv_data = NULL; + return 0; +} + +} // namespace + +//------------------------------------------------------------------------------ + +namespace media { + +// Use the HTTP protocol to avoid any file path separator issues. +static const char kProtocol[] = "http"; + +// Fill out our FFmpeg protocol definition. +static URLProtocol kFFmpegProtocol = { + kProtocol, + &OpenContext, + &ReadContext, + &WriteContext, + &SeekContext, + &CloseContext, +}; + +FFmpegGlue::FFmpegGlue() { + // Register our protocol glue code with FFmpeg. + avcodec_init(); + register_protocol(&kFFmpegProtocol); + + // Now register the rest of FFmpeg. + av_register_all(); +} + +FFmpegGlue::~FFmpegGlue() { + DataSourceMap::iterator iter = data_sources_.begin(); + while (iter != data_sources_.end()) { + DataSource* data_source = iter->second; + iter = data_sources_.erase(iter); + } +} + +std::string FFmpegGlue::AddDataSource(DataSource* data_source) { + AutoLock auto_lock(lock_); + std::string key = GetDataSourceKey(data_source); + if (data_sources_.find(key) == data_sources_.end()) { + data_sources_[key] = data_source; + } + return key; +} + +void FFmpegGlue::RemoveDataSource(DataSource* data_source) { + AutoLock auto_lock(lock_); + DataSourceMap::iterator iter = data_sources_.begin(); + while (iter != data_sources_.end()) { + if (iter->second == data_source) { + iter = data_sources_.erase(iter); + } else { + ++iter; + } + } +} + +void FFmpegGlue::GetDataSource(const std::string& key, + scoped_refptr<DataSource>* data_source) { + AutoLock auto_lock(lock_); + DataSourceMap::iterator iter = data_sources_.find(key); + if (iter == data_sources_.end()) { + *data_source = NULL; + return; + } + *data_source = iter->second; +} + +std::string FFmpegGlue::GetDataSourceKey(DataSource* data_source) { + // Use the DataSource's memory address to generate the unique string. This + // also has the nice property that adding the same DataSource reference will + // not generate duplicate entries. + return StringPrintf("%s://0x%lx", kProtocol, static_cast<void*>(data_source)); +} + +} // namespace media diff --git a/media/filters/ffmpeg_glue.h b/media/filters/ffmpeg_glue.h new file mode 100644 index 0000000..0db1ce8 --- /dev/null +++ b/media/filters/ffmpeg_glue.h @@ -0,0 +1,80 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// FFmpegGlue is an adapter for FFmpeg's URLProtocol interface that allows us to +// use a DataSource implementation with FFmpeg. For convenience we use FFmpeg's +// av_open_input_file function, which analyzes the filename given to it and +// automatically initializes the appropriate URLProtocol. +// +// Since the DataSource is already open by time we call av_open_input_file, we +// need a way for av_open_input_file to find the correct DataSource instance. +// The solution is to maintain a map of "filenames" to DataSource instances, +// where filenames are actually just a unique identifier. For simplicity, +// FFmpegGlue is registered as an HTTP handler and generates filenames based on +// the memory address of the DataSource, i.e., http://0xc0bf4870. Since there +// may be multiple FFmpegDemuxers active at one time, FFmpegGlue is a +// thread-safe singleton. +// +// Usage: FFmpegDemuxer adds the DataSource to FFmpegGlue's map and is given a +// filename to pass to av_open_input_file. FFmpegDemuxer calls +// av_open_input_file with the filename, which results in FFmpegGlue returning +// the DataSource as a URLProtocol instance to FFmpeg. Since FFmpegGlue is only +// needed for opening files, when av_open_input_file returns FFmpegDemuxer +// removes the DataSource from FFmpegGlue's map. + +#ifndef MEDIA_FILTERS_FFMPEG_GLUE_H_ +#define MEDIA_FILTERS_FFMPEG_GLUE_H_ + +#include <map> +#include <string> + +#include "base/lock.h" +#include "base/singleton.h" + +// FFmpeg forward declarations. +struct URLContext; +typedef int64 offset_t; + +namespace media { + +class DataSource; + +class FFmpegGlue : public Singleton<FFmpegGlue> { + public: + // Adds a DataSource to the FFmpeg glue layer and returns a unique string that + // can be passed to FFmpeg to identify the data source. + std::string AddDataSource(DataSource* data_source); + + // Removes a DataSource from the FFmpeg glue layer. Using strings from + // previously added DataSources will no longer work. + void RemoveDataSource(DataSource* data_source); + + // Assigns the DataSource identified with by the given key to |data_source|, + // or assigns NULL if no such DataSource could be found. + void GetDataSource(const std::string& key, + scoped_refptr<DataSource>* data_source); + + private: + // Only allow Singleton to create and delete FFmpegGlue. + friend struct DefaultSingletonTraits<FFmpegGlue>; + FFmpegGlue(); + virtual ~FFmpegGlue(); + + // Returns the unique key for this data source, which can be passed to + // av_open_input_file as the filename. + std::string GetDataSourceKey(DataSource* data_source); + + // Mutual exclusion while adding/removing items from the map. + Lock lock_; + + // Map between keys and DataSource references. + typedef std::map< std::string, scoped_refptr<DataSource> > DataSourceMap; + DataSourceMap data_sources_; + + DISALLOW_COPY_AND_ASSIGN(FFmpegGlue); +}; + +} // namespace media + +#endif // MEDIA_FILTERS_FFMPEG_GLUE_H_ diff --git a/media/filters/ffmpeg_glue_unittest.cc b/media/filters/ffmpeg_glue_unittest.cc new file mode 100644 index 0000000..d1d2a83 --- /dev/null +++ b/media/filters/ffmpeg_glue_unittest.cc @@ -0,0 +1,309 @@ +// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "media/base/filters.h" +#include "media/base/mock_media_filters.h" +#include "media/filters/ffmpeg_common.h" +#include "media/filters/ffmpeg_glue.h" +#include "testing/gtest/include/gtest/gtest.h" + +// FFmpeg mocks to remove dependency on having the DLLs present. +extern "C" { +static bool g_avcodec_init = false; +static URLProtocol* g_protocol = NULL; +static bool g_av_register_all = false; + +void avcodec_init() { + EXPECT_FALSE(g_avcodec_init); + g_avcodec_init = true; +} + +int register_protocol(URLProtocol* protocol) { + EXPECT_FALSE(g_protocol); + g_protocol = protocol; + return 0; +} + +void av_register_all() { + EXPECT_FALSE(g_av_register_all); + g_av_register_all = true; +} +} // extern "C" + +TEST(FFmpegGlueTest, InitializeFFmpeg) { + // Singleton should initialize FFmpeg. + media::FFmpegGlue* glue = media::FFmpegGlue::get(); + EXPECT_TRUE(glue); + EXPECT_TRUE(g_avcodec_init); + EXPECT_TRUE(g_protocol); + EXPECT_TRUE(g_av_register_all); + + // Make sure URLProtocol was filled out correctly. + EXPECT_STREQ("http", g_protocol->name); + EXPECT_TRUE(g_protocol->url_close); + EXPECT_TRUE(g_protocol->url_open); + EXPECT_TRUE(g_protocol->url_read); + EXPECT_TRUE(g_protocol->url_seek); + EXPECT_TRUE(g_protocol->url_write); +} + +TEST(FFmpegGlueTest, AddRemoveGetDataSource) { + // Prepare testing data. + media::FFmpegGlue* glue = media::FFmpegGlue::get(); + + // Create our data sources and add them to the glue layer. + bool deleted_a = false; + bool deleted_b = false; + media::MockFilterConfig config_a; + media::MockFilterConfig config_b; + scoped_refptr<media::MockDataSource> data_source_a + = new media::MockDataSource(&config_a, &deleted_a); + scoped_refptr<media::MockDataSource> data_source_b + = new media::MockDataSource(&config_b, &deleted_b); + + // Make sure the keys are unique. + std::string key_a = glue->AddDataSource(data_source_a); + std::string key_b = glue->AddDataSource(data_source_b); + EXPECT_EQ(0, key_a.find("http://")); + EXPECT_EQ(0, key_b.find("http://")); + EXPECT_NE(key_a, key_b); + + // Our keys should return our data sources. + scoped_refptr<media::DataSource> data_source_c; + scoped_refptr<media::DataSource> data_source_d; + glue->GetDataSource(key_a, &data_source_c); + glue->GetDataSource(key_b, &data_source_d); + EXPECT_EQ(data_source_a, data_source_c); + EXPECT_EQ(data_source_b, data_source_d); + + // Adding the same DataSource should create the same key and not add an extra + // reference. + std::string key_a2 = glue->AddDataSource(data_source_a); + EXPECT_EQ(key_a, key_a2); + glue->GetDataSource(key_a2, &data_source_c); + EXPECT_EQ(data_source_a, data_source_c); + + // Removes the data sources and then releases our references. They should be + // deleted. + glue->RemoveDataSource(data_source_a); + glue->GetDataSource(key_a, &data_source_c); + EXPECT_FALSE(data_source_c); + glue->GetDataSource(key_b, &data_source_d); + EXPECT_EQ(data_source_b, data_source_d); + glue->RemoveDataSource(data_source_b); + glue->GetDataSource(key_b, &data_source_d); + EXPECT_FALSE(data_source_d); + EXPECT_FALSE(deleted_a); + EXPECT_FALSE(deleted_b); + data_source_a = NULL; + data_source_b = NULL; + EXPECT_TRUE(deleted_a); + EXPECT_TRUE(deleted_b); +} + +TEST(FFmpegGlueTest, OpenClose) { + // Prepare testing data. + media::FFmpegGlue* glue = media::FFmpegGlue::get(); + + // Create our data source and add them to the glue layer. + bool deleted = false; + media::MockFilterConfig config; + scoped_refptr<media::MockDataSource> data_source + = new media::MockDataSource(&config, &deleted); + std::string key = glue->AddDataSource(data_source); + + // Prepare FFmpeg URLContext structure. + URLContext context; + memset(&context, 0, sizeof(context)); + + // Test opening a URLContext with a data source that doesn't exist. + EXPECT_EQ(AVERROR_IO, g_protocol->url_open(&context, "foobar", 0)); + + // Test opening a URLContext with our data source. + EXPECT_EQ(0, g_protocol->url_open(&context, key.c_str(), 0)); + EXPECT_EQ(URL_RDONLY, context.flags); + EXPECT_EQ(data_source, context.priv_data); + EXPECT_FALSE(context.is_streamed); + + // Remove the data source from the glue layer, releasing a reference. + glue->RemoveDataSource(data_source); + EXPECT_FALSE(deleted); + + // Remove our own reference -- URLContext should maintain a reference. + data_source = NULL; + EXPECT_FALSE(deleted); + + // Close the URLContext, which should release the final reference. + EXPECT_EQ(0, g_protocol->url_close(&context)); + EXPECT_TRUE(deleted); +} + +TEST(FFmpegGlueTest, ReadingWriting) { + // Prepare testing data. + media::FFmpegGlue* glue = media::FFmpegGlue::get(); + const size_t kBufferSize = 16; + unsigned char buffer[kBufferSize]; + + // Configure MockDataSource to be 8 characters long and fill reads with + // periods. Therefore our expected string should be a character of 8 periods. + const size_t kExpectedSize = 8; + media::MockFilterConfig config; + config.media_total_bytes = kExpectedSize; + config.data_source_value = '.'; + const char kExpected[] = "........"; + COMPILE_ASSERT(kExpectedSize == (arraysize(kExpected) - 1), string_length); + + // Create our data source and add them to the glue layer. + bool deleted = false; + scoped_refptr<media::MockDataSource> data_source + = new media::MockDataSource(&config, &deleted); + std::string key = glue->AddDataSource(data_source); + + // Open our data source and then remove it from the glue layer. + URLContext context; + memset(&context, 0, sizeof(context)); + EXPECT_EQ(0, g_protocol->url_open(&context, key.c_str(), 0)); + glue->RemoveDataSource(data_source); + EXPECT_FALSE(deleted); + + // Writing should always fail. + EXPECT_EQ(AVERROR_IO, g_protocol->url_write(&context, NULL, 0)); + EXPECT_EQ(AVERROR_IO, g_protocol->url_write(&context, buffer, 0)); + EXPECT_EQ(AVERROR_IO, g_protocol->url_write(&context, buffer, -1)); + EXPECT_EQ(AVERROR_IO, g_protocol->url_write(&context, buffer, kBufferSize)); + EXPECT_EQ(0, data_source->position()); + + // Reading should return same amount of bytes if <= kExpectedSize. + EXPECT_EQ(0, g_protocol->url_read(&context, buffer, 0)); + EXPECT_EQ(kExpectedSize / 2, + g_protocol->url_read(&context, buffer, kExpectedSize / 2)); + EXPECT_EQ(kExpectedSize, + g_protocol->url_read(&context, buffer, kExpectedSize)); + buffer[kExpectedSize] = '\0'; + EXPECT_STREQ(kExpected, reinterpret_cast<char*>(buffer)); + + // Test reading more than kExpectedSize for simulating EOF. + EXPECT_EQ(kExpectedSize, g_protocol->url_read(&context, buffer, kBufferSize)); + buffer[kExpectedSize] = '\0'; + EXPECT_STREQ(kExpected, reinterpret_cast<char*>(buffer)); + + // Close our data source. + EXPECT_EQ(0, g_protocol->url_close(&context)); + EXPECT_FALSE(deleted); + + // Remove our own reference, which should release the final reference. + data_source = NULL; + EXPECT_TRUE(deleted); +} + +TEST(FFmpegGlueTest, Seeking) { + // Prepare testing data. + media::FFmpegGlue* glue = media::FFmpegGlue::get(); + const int64 kSize = 32; + + // Create our data source and add them to the glue layer. + bool deleted = false; + media::MockFilterConfig config; + config.media_total_bytes = kSize; + scoped_refptr<media::MockDataSource> data_source + = new media::MockDataSource(&config, &deleted); + std::string key = glue->AddDataSource(data_source); + + // Open our data source and then remove it from the glue layer. + URLContext context; + memset(&context, 0, sizeof(context)); + EXPECT_EQ(0, g_protocol->url_open(&context, key.c_str(), 0)); + glue->RemoveDataSource(data_source); + EXPECT_FALSE(deleted); + + // Test SEEK_SET operations. + config.media_total_bytes = -1; + EXPECT_EQ(AVERROR_IO, g_protocol->url_seek(&context, 0, SEEK_SET)); + + config.media_total_bytes = kSize; + EXPECT_TRUE(data_source->SetPosition(0)); + EXPECT_EQ(0, g_protocol->url_seek(&context, 0, SEEK_SET)); + EXPECT_TRUE(data_source->SetPosition(5)); + EXPECT_EQ(0, g_protocol->url_seek(&context, 0, SEEK_SET)); + EXPECT_EQ(0, data_source->position()); + EXPECT_EQ(AVERROR_IO, g_protocol->url_seek(&context, -5, SEEK_SET)); + EXPECT_EQ(0, data_source->position()); + EXPECT_EQ(kSize, g_protocol->url_seek(&context, kSize, SEEK_SET)); + EXPECT_EQ(kSize, data_source->position()); + EXPECT_EQ(AVERROR_IO, g_protocol->url_seek(&context, kSize+1, SEEK_SET)); + EXPECT_EQ(kSize, data_source->position()); + + // Test SEEK_CUR operations. + config.media_total_bytes = -1; + EXPECT_EQ(AVERROR_IO, g_protocol->url_seek(&context, 0, SEEK_CUR)); + + config.media_total_bytes = kSize; + EXPECT_TRUE(data_source->SetPosition(0)); + EXPECT_EQ(0, g_protocol->url_seek(&context, 0, SEEK_CUR)); + EXPECT_TRUE(data_source->SetPosition(5)); + EXPECT_EQ(5, g_protocol->url_seek(&context, 0, SEEK_CUR)); + EXPECT_EQ(0, g_protocol->url_seek(&context, -5, SEEK_CUR)); + EXPECT_EQ(0, data_source->position()); + EXPECT_EQ(AVERROR_IO, g_protocol->url_seek(&context, -1, SEEK_CUR)); + EXPECT_EQ(kSize, g_protocol->url_seek(&context, kSize, SEEK_CUR)); + EXPECT_EQ(kSize, data_source->position()); + EXPECT_EQ(AVERROR_IO, g_protocol->url_seek(&context, 1, SEEK_CUR)); + EXPECT_EQ(kSize, data_source->position()); + + // Test SEEK_END operations. + config.media_total_bytes = -1; + EXPECT_EQ(AVERROR_IO, g_protocol->url_seek(&context, 0, SEEK_END)); + + config.media_total_bytes = kSize; + EXPECT_TRUE(data_source->SetPosition(0)); + EXPECT_EQ(kSize, g_protocol->url_seek(&context, 0, SEEK_END)); + EXPECT_EQ(kSize, data_source->position()); + EXPECT_EQ(kSize-5, g_protocol->url_seek(&context, -5, SEEK_END)); + EXPECT_EQ(kSize-5, data_source->position()); + EXPECT_EQ(0, g_protocol->url_seek(&context, -kSize, SEEK_END)); + EXPECT_EQ(0, data_source->position()); + EXPECT_EQ(AVERROR_IO, g_protocol->url_seek(&context, 1, SEEK_END)); + EXPECT_EQ(0, data_source->position()); + + // Test AVSEEK_SIZE operation. + config.media_total_bytes = -1; + EXPECT_EQ(AVERROR_IO, g_protocol->url_seek(&context, 0, AVSEEK_SIZE)); + + config.media_total_bytes = kSize; + EXPECT_TRUE(data_source->SetPosition(0)); + EXPECT_EQ(kSize, g_protocol->url_seek(&context, 0, AVSEEK_SIZE)); + + // Close our data source. + EXPECT_EQ(0, g_protocol->url_close(&context)); + EXPECT_FALSE(deleted); + + // Remove our own reference, which should release the final reference. + data_source = NULL; + EXPECT_TRUE(deleted); +} + +TEST(FFmpegGlueTest, Destructor) { + // Prepare testing data. + media::FFmpegGlue* glue = media::FFmpegGlue::get(); + + // We use a static bool since ~FFmpegGlue() will set it to true sometime + // after this function exits. + static bool deleted = false; + + // Create our data source and add them to the glue layer. + media::MockFilterConfig config; + scoped_refptr<media::MockDataSource> data_source + = new media::MockDataSource(&config, &deleted); + std::string key = glue->AddDataSource(data_source); + + // Remove our own reference. + data_source = NULL; + EXPECT_FALSE(deleted); + + // ~FFmpegGlue() will be called when this unit test finishes execution. By + // leaving something inside FFmpegGlue's map we get to test our cleanup code. + // + // MockDataSource will be holding onto a bad MockFilterConfig pointer at this + // point but since no one is calling it everything will be ok. +} |