| // Copyright (c) 2012 The WebM project authors. All Rights Reserved. |
| // |
| // Use of this source code is governed by a BSD-style license |
| // that can be found in the LICENSE file in the root of the source |
| // tree. An additional intellectual property rights grant can be found |
| // in the file PATENTS. All contributing project authors may |
| // be found in the AUTHORS file in the root of the source tree. |
| |
| #include "encoder/webm_mux.h" |
| |
| #include <new> |
| #include <vector> |
| |
| #include "glog/logging.h" |
| #include "libwebm/mkvmuxer.hpp" |
| #include "libwebm/webmids.hpp" |
| |
| namespace { |
| const int kAutoAssignTrackNum = 0; |
| } // namespace |
| |
| namespace webmlive { |
| |
| template <typename T> |
| T milliseconds_to_timecode_ticks(T milliseconds) { |
| return milliseconds * LiveWebmMuxer::kTimecodeScale; |
| } |
| |
| // Buffer object implementing libwebm's IMkvWriter interface. |
| class WebmMuxWriter : public mkvmuxer::IMkvWriter { |
| public: |
| enum { |
| kNotImplemented = -200, |
| kNotInitialized = -2, |
| kInvalidArg = -1, |
| kSuccess = 0, |
| }; |
| WebmMuxWriter(); |
| virtual ~WebmMuxWriter(); |
| |
| // Stores |ptr_buffer| and returns |kSuccess|. |
| int32 Init(LiveWebmMuxer::WriteBuffer* ptr_write_buffer, |
| const std::string& id); |
| |
| // Accessors. |
| int64 bytes_written() const { return bytes_written_; } |
| int64 chunk_end() const { return chunk_end_; } |
| |
| // Erases chunk from |ptr_write_buffer_|, resets |chunk_end_| to 0, and |
| // updates |bytes_buffered_|. |
| void EraseChunk(); |
| |
| // mkvmuxer::IMkvWriter methods |
| // Returns total bytes of data passed to |Write|. |
| virtual int64 Position() const { return bytes_written_; } |
| |
| // Not seekable, return |kNotImplemented| on seek attempts. |
| virtual int32 Position(int64) { return kNotImplemented; } // NOLINT |
| |
| // Always returns false: |WebmMuxWriter| is never seekable. Written data |
| // goes into a vector, and data is buffered only until a chunk is completed. |
| virtual bool Seekable() const { return false; } |
| |
| // Writes |ptr_buffer| contents to |ptr_write_buffer_|. |
| virtual int32 Write(const void* ptr_buffer, uint32 buffer_length); |
| |
| // Called by libwebm, and notifies writer of element start position. |
| virtual void ElementStartNotify(uint64 element_id, int64 position); |
| |
| private: |
| int64 bytes_buffered_; |
| int64 bytes_written_; |
| int64 chunk_end_; |
| LiveWebmMuxer::WriteBuffer* ptr_write_buffer_; |
| std::string id_; |
| WEBMLIVE_DISALLOW_COPY_AND_ASSIGN(WebmMuxWriter); |
| }; |
| |
| WebmMuxWriter::WebmMuxWriter() |
| : bytes_buffered_(0), |
| bytes_written_(0), |
| chunk_end_(0), |
| ptr_write_buffer_(NULL) { |
| } |
| |
| WebmMuxWriter::~WebmMuxWriter() { |
| } |
| |
| int32 WebmMuxWriter::Init(LiveWebmMuxer::WriteBuffer* ptr_write_buffer, |
| const std::string& id) { |
| if (!ptr_write_buffer) { |
| LOG(ERROR) << "Cannot Init, NULL write buffer."; |
| return kInvalidArg; |
| } |
| ptr_write_buffer_ = ptr_write_buffer; |
| id_ = id; |
| return kSuccess; |
| } |
| |
| void WebmMuxWriter::EraseChunk() { |
| if (ptr_write_buffer_) { |
| LiveWebmMuxer::WriteBuffer::iterator erase_end_pos = |
| ptr_write_buffer_->begin() + static_cast<int32>(chunk_end_); |
| ptr_write_buffer_->erase(ptr_write_buffer_->begin(), erase_end_pos); |
| bytes_buffered_ = ptr_write_buffer_->size(); |
| chunk_end_ = 0; |
| } |
| } |
| |
| int32 WebmMuxWriter::Write(const void* ptr_buffer, uint32 buffer_length) { |
| if (!ptr_write_buffer_) { |
| LOG(ERROR) << "Cannot Write, not Initialized."; |
| return kNotInitialized; |
| } |
| if (!ptr_buffer || !buffer_length) { |
| LOG(ERROR) << "returning kInvalidArg to libwebm: NULL/0 length buffer."; |
| return kInvalidArg; |
| } |
| const uint8* ptr_data = reinterpret_cast<const uint8*>(ptr_buffer); |
| ptr_write_buffer_->insert(ptr_write_buffer_->end(), |
| ptr_data, |
| ptr_data + buffer_length); |
| bytes_written_ += buffer_length; |
| bytes_buffered_ = ptr_write_buffer_->size(); |
| return kSuccess; |
| } |
| |
| void WebmMuxWriter::ElementStartNotify(uint64 element_id, int64 position) { |
| if (element_id == mkvmuxer::kMkvCluster) { |
| chunk_end_ = bytes_buffered_; |
| if (id_ == "video") { |
| LOG(INFO) << "video chunk_end_=" << chunk_end_<< " position=" << position; |
| } |
| } |
| } |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| // LiveWebmMuxer |
| // |
| |
| LiveWebmMuxer::LiveWebmMuxer() |
| : audio_track_num_(0), |
| video_track_num_(0), |
| muxer_time_(0), |
| chunks_read_(0) { |
| } |
| |
| LiveWebmMuxer::~LiveWebmMuxer() { |
| } |
| |
| int LiveWebmMuxer::Init(int32 cluster_duration_milliseconds, |
| const std::string& muxer_id) { |
| muxer_id_ = muxer_id; |
| |
| // Construct and Init |WebmMuxWriter|-- it handles writes coming from libwebm. |
| ptr_writer_.reset(new (std::nothrow) WebmMuxWriter()); // NOLINT |
| if (!ptr_writer_) { |
| LOG(ERROR) << "cannot construct WebmWriteBuffer."; |
| return kNoMemory; |
| } |
| if (ptr_writer_->Init(&buffer_, muxer_id)) { |
| LOG(ERROR) << "cannot Init WebmWriteBuffer."; |
| return kMuxerError; |
| } |
| |
| // Construct and Init |ptr_segment_|, then enable live mode. |
| ptr_segment_.reset(new (std::nothrow) mkvmuxer::Segment()); // NOLINT |
| if (!ptr_segment_) { |
| LOG(ERROR) << "cannot construct Segment."; |
| return kNoMemory; |
| } |
| |
| if (!ptr_segment_->Init(ptr_writer_.get())) { |
| LOG(ERROR) << "cannot Init Segment."; |
| return kMuxerError; |
| } |
| |
| ptr_segment_->set_mode(mkvmuxer::Segment::kLive); |
| if (cluster_duration_milliseconds > 0) { |
| const uint64 max_cluster_duration = |
| milliseconds_to_timecode_ticks(cluster_duration_milliseconds); |
| ptr_segment_->set_max_cluster_duration(max_cluster_duration); |
| } |
| |
| // Set segment info fields. |
| using mkvmuxer::SegmentInfo; |
| SegmentInfo* const ptr_segment_info = ptr_segment_->GetSegmentInfo(); |
| if (!ptr_segment_info) { |
| LOG(ERROR) << "Segment has no SegmentInfo."; |
| return kNoMemory; |
| } |
| ptr_segment_info->set_timecode_scale(kTimecodeScale); |
| |
| // Set writing application name. |
| std::string app_name = kEncoderName; |
| app_name += " v"; |
| app_name += kEncoderVersion; |
| ptr_segment_info->set_writing_app(app_name.c_str()); |
| return kSuccess; |
| } |
| |
| int LiveWebmMuxer::AddTrack(const AudioConfig& audio_config, |
| const VorbisCodecPrivate& codec_private) { |
| if (audio_track_num_ != 0) { |
| LOG(ERROR) << "Cannot add audio track: it already exists."; |
| return kAudioTrackAlreadyExists; |
| } |
| const VorbisCodecPrivate& vcp = codec_private; |
| |
| // Perform minimal private data validation. |
| if (!vcp.ptr_ident || !vcp.ptr_comments || !vcp.ptr_setup) { |
| LOG(ERROR) << "Cannot add audio track: NULL private data contents."; |
| return kAudioPrivateDataInvalid; |
| } |
| if (vcp.ident_length > 255 || vcp.comments_length > 255) { |
| LOG(ERROR) << "Cannot add audio track: over maximum ident/comment length."; |
| return kAudioPrivateDataInvalid; |
| } |
| const int data_length = |
| vcp.ident_length + vcp.comments_length + vcp.setup_length; |
| |
| // Calculate total bytes of storage required for the private data chunk. |
| // 1 byte to store header count (total headers - 1 = 2). |
| // 1 byte each for ident and comment length values. |
| // The length of setup data is implied by the total length. |
| const int header_length = 1 + 1 + 1 + data_length; |
| std::unique_ptr<uint8[]> private_data; |
| private_data.reset(new (std::nothrow) uint8[header_length]); |
| if (!private_data) { |
| LOG(ERROR) << "Cannot allocate private data block"; |
| return kNoMemory; |
| } |
| uint8* ptr_private_data = private_data.get(); |
| |
| // Write header count. As above, number of headers - 1. |
| *ptr_private_data++ = 2; |
| |
| // Write ident length, comment length. |
| *ptr_private_data++ = static_cast<uint8>(vcp.ident_length); |
| *ptr_private_data++ = static_cast<uint8>(vcp.comments_length); |
| |
| // Write the data blocks. |
| memcpy(ptr_private_data, vcp.ptr_ident, vcp.ident_length); |
| ptr_private_data += vcp.ident_length; |
| memcpy(ptr_private_data, vcp.ptr_comments, vcp.comments_length); |
| ptr_private_data += vcp.comments_length; |
| memcpy(ptr_private_data, vcp.ptr_setup, vcp.setup_length); |
| |
| audio_track_num_ = ptr_segment_->AddAudioTrack(audio_config.sample_rate, |
| audio_config.channels, |
| kAutoAssignTrackNum); |
| if (!audio_track_num_) { |
| LOG(ERROR) << "cannot AddAudioTrack on segment."; |
| return kVideoTrackError; |
| } |
| mkvmuxer::AudioTrack* const ptr_audio_track = |
| static_cast<mkvmuxer::AudioTrack*>( |
| ptr_segment_->GetTrackByNumber(audio_track_num_)); |
| if (!ptr_audio_track) { |
| LOG(ERROR) << "Unable to access audio track."; |
| return kAudioTrackError; |
| } |
| if (!ptr_audio_track->SetCodecPrivate(private_data.get(), header_length)) { |
| LOG(ERROR) << "Unable to write audio track codec private data."; |
| return kAudioTrackError; |
| } |
| return kSuccess; |
| } |
| |
| int LiveWebmMuxer::AddTrack(const VideoConfig& video_config) { |
| if (video_track_num_ != 0) { |
| LOG(ERROR) << "Cannot add video track: it already exists."; |
| return kVideoTrackAlreadyExists; |
| } |
| video_track_num_ = ptr_segment_->AddVideoTrack(video_config.width, |
| video_config.height, |
| kAutoAssignTrackNum); |
| if (!video_track_num_) { |
| LOG(ERROR) << "cannot AddVideoTrack on segment."; |
| return kVideoTrackError; |
| } |
| |
| if (video_config.format != kVideoFormatVP8) { |
| mkvmuxer::VideoTrack* const video_track = |
| static_cast<mkvmuxer::VideoTrack*>( |
| ptr_segment_->GetTrackByNumber(video_track_num_)); |
| if (!video_track) { |
| LOG(ERROR) << "cannot get video track to set codec.\n"; |
| return kVideoTrackError; |
| } |
| video_track->set_codec_id(mkvmuxer::Tracks::kVp9CodecId); |
| } |
| |
| return kSuccess; |
| } |
| |
| int LiveWebmMuxer::Finalize() { |
| if (!ptr_segment_->Finalize()) { |
| LOG(ERROR) << "libwebm mkvmuxer Finalize failed."; |
| return kMuxerError; |
| } |
| |
| if (buffer_.size() > 0) { |
| // When data is in |buffer_| after the |mkvmuxer::Segment::Finalize()| |
| // call, make the last chunk available to the user by forcing |
| // |ChunkReady()| to return true one final time. This last chunk will |
| // contain any data passed to |mkvmuxer::Segment::AddFrame()| since the |
| // last call to |WebmMuxWriter::ElementStartNotify()|. |
| ptr_writer_->ElementStartNotify(mkvmuxer::kMkvCluster, |
| ptr_writer_->bytes_written()); |
| } |
| |
| return kSuccess; |
| } |
| |
| int LiveWebmMuxer::WriteVideoFrame(const VideoFrame& vpx_frame) { |
| if (video_track_num_ == 0) { |
| LOG(ERROR) << "Cannot WriteVideoFrame without a video track."; |
| return kNoVideoTrack; |
| } |
| if (!vpx_frame.buffer()) { |
| LOG(ERROR) << "cannot write empty frame."; |
| return kInvalidArg; |
| } |
| if (vpx_frame.format() != kVideoFormatVP8 && |
| vpx_frame.format() != kVideoFormatVP9) { |
| LOG(ERROR) << "cannot write non-VPx frame."; |
| return kInvalidArg; |
| } |
| const int64 timecode = milliseconds_to_timecode_ticks(vpx_frame.timestamp()); |
| if (!ptr_segment_->AddFrame(vpx_frame.buffer(), |
| vpx_frame.buffer_length(), |
| video_track_num_, |
| timecode, |
| vpx_frame.keyframe())) { |
| LOG(ERROR) << "AddFrame (video) failed."; |
| return kVideoWriteError; |
| } |
| muxer_time_ = vpx_frame.timestamp(); |
| return kSuccess; |
| } |
| |
| int LiveWebmMuxer::WriteAudioBuffer(const AudioBuffer& vorbis_buffer) { |
| if (audio_track_num_ == 0) { |
| LOG(ERROR) << "Cannot WriteAudioBuffer without an audio track."; |
| return kNoAudioTrack; |
| } |
| if (!vorbis_buffer.buffer()) { |
| LOG(ERROR) << "cannot write empty audio buffer."; |
| return kInvalidArg; |
| } |
| if (vorbis_buffer.config().format_tag != kAudioFormatVorbis) { |
| LOG(ERROR) << "cannot write non-Vorbis audio buffer."; |
| return kInvalidArg; |
| } |
| const int64 timecode = |
| milliseconds_to_timecode_ticks(vorbis_buffer.timestamp()); |
| if (!ptr_segment_->AddFrame(vorbis_buffer.buffer(), |
| vorbis_buffer.buffer_length(), |
| audio_track_num_, |
| timecode, |
| true)) { |
| LOG(ERROR) << "AddFrame (audio) failed."; |
| return kAudioWriteError; |
| } |
| muxer_time_ = vorbis_buffer.timestamp(); |
| return kSuccess; |
| } |
| |
| // A chunk is ready when |WebmMuxWriter::chunk_length()| returns a value |
| // greater than 0. |
| bool LiveWebmMuxer::ChunkReady(int32* ptr_chunk_length) { |
| if (ptr_chunk_length) { |
| const int32 chunk_length = static_cast<int32>(ptr_writer_->chunk_end()); |
| if (chunk_length > 0) { |
| *ptr_chunk_length = chunk_length; |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| // Copies the buffered chunk data into |ptr_buf|, erases it from |buffer_|, and |
| // calls |WebmMuxWriter::ResetChunkEnd()| to zero the chunk end position. |
| int LiveWebmMuxer::ReadChunk(int32 buffer_capacity, uint8* ptr_buf) { |
| if (!ptr_buf) { |
| LOG(ERROR) << "NULL buffer pointer."; |
| return kInvalidArg; |
| } |
| |
| // Make sure there's a chunk ready. |
| int32 chunk_length = 0; |
| if (!ChunkReady(&chunk_length)) { |
| LOG(ERROR) << "No chunk ready."; |
| return kNoChunkReady; |
| } |
| |
| // Confirm user buffer is of adequate size |
| if (buffer_capacity < chunk_length) { |
| LOG(ERROR) << "Not enough space for chunk."; |
| return kUserBufferTooSmall; |
| } |
| |
| LOG(INFO) << "ReadChunk capacity=" << buffer_capacity |
| << " length=" << chunk_length |
| << " total buffered=" << buffer_.size(); |
| |
| // Copy chunk to user buffer, and erase it from |buffer_|. |
| memcpy(ptr_buf, &buffer_[0], chunk_length); |
| ptr_writer_->EraseChunk(); |
| ++chunks_read_; |
| return kSuccess; |
| } |
| |
| } // namespace webmlive |