/* * Copyright (c) 2011 The WebRTC 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 "engine_configurations.h" #include "file_recorder_impl.h" #include "media_file.h" #include "trace.h" #ifdef WEBRTC_MODULE_UTILITY_VIDEO #include "cpu_wrapper.h" #include "critical_section_wrapper.h" #include "frame_scaler.h" #include "video_coder.h" #include "video_frames_queue.h" #endif // OS independent case insensitive string comparison. #ifdef WIN32 #define STR_CASE_CMP(x,y) ::_stricmp(x,y) #else #define STR_CASE_CMP(x,y) ::strcasecmp(x,y) #endif namespace webrtc { FileRecorder* FileRecorder::CreateFileRecorder(WebRtc_UWord32 instanceID, FileFormats fileFormat) { switch(fileFormat) { case kFileFormatWavFile: case kFileFormatCompressedFile: case kFileFormatPreencodedFile: case kFileFormatPcm16kHzFile: case kFileFormatPcm8kHzFile: case kFileFormatPcm32kHzFile: return new FileRecorderImpl(instanceID, fileFormat); #ifdef WEBRTC_MODULE_UTILITY_VIDEO case kFileFormatAviFile: return new AviRecorder(instanceID, fileFormat); #endif default: return NULL; } } void FileRecorder::DestroyFileRecorder(FileRecorder* recorder) { delete recorder; } FileRecorderImpl::FileRecorderImpl(WebRtc_UWord32 instanceID, FileFormats fileFormat) : _fileFormat(fileFormat), _instanceID(instanceID), _audioEncoder(instanceID), _amrFormat(AMRFileStorage), _moduleFile(MediaFile::CreateMediaFile(_instanceID)) { } FileRecorderImpl::~FileRecorderImpl() { MediaFile::DestroyMediaFile(_moduleFile); } FileFormats FileRecorderImpl::RecordingFileFormat() const { return _fileFormat; } WebRtc_Word32 FileRecorderImpl::RegisterModuleFileCallback( FileCallback* callback) { if(_moduleFile == NULL) { return -1; } return _moduleFile->SetModuleFileCallback(callback); } WebRtc_Word32 FileRecorderImpl::StartRecordingAudioFile( const WebRtc_Word8* fileName, const CodecInst& codecInst, WebRtc_UWord32 notificationTimeMs, ACMAMRPackingFormat amrFormat) { if(_moduleFile == NULL) { return -1; } codec_info_ = codecInst; _amrFormat = amrFormat; WebRtc_Word32 retVal = 0; if(_fileFormat != kFileFormatAviFile) { // AVI files should be started using StartRecordingVideoFile(..) all // other formats should use this API. retVal =_moduleFile->StartRecordingAudioFile(fileName, _fileFormat, codecInst, notificationTimeMs); } if( retVal == 0) { retVal = SetUpAudioEncoder(); } if( retVal != 0) { WEBRTC_TRACE( kTraceWarning, kTraceVoice, _instanceID, "FileRecorder::StartRecording() failed to initialize file %s for\ recording.", fileName); if(IsRecording()) { StopRecording(); } } return retVal; } WebRtc_Word32 FileRecorderImpl::StartRecordingAudioFile( OutStream& destStream, const CodecInst& codecInst, WebRtc_UWord32 notificationTimeMs, ACMAMRPackingFormat amrFormat) { codec_info_ = codecInst; _amrFormat = amrFormat; WebRtc_Word32 retVal = _moduleFile->StartRecordingAudioStream( destStream, _fileFormat, codecInst, notificationTimeMs); if( retVal == 0) { retVal = SetUpAudioEncoder(); } if( retVal != 0) { WEBRTC_TRACE( kTraceWarning, kTraceVoice, _instanceID, "FileRecorder::StartRecording() failed to initialize outStream for\ recording."); if(IsRecording()) { StopRecording(); } } return retVal; } WebRtc_Word32 FileRecorderImpl::StopRecording() { memset(&codec_info_, 0, sizeof(CodecInst)); return _moduleFile->StopRecording(); } bool FileRecorderImpl::IsRecording() const { return _moduleFile->IsRecording(); } WebRtc_Word32 FileRecorderImpl::RecordAudioToFile( const AudioFrame& incomingAudioFrame, const TickTime* playoutTS) { if (codec_info_.plfreq == 0) { WEBRTC_TRACE( kTraceWarning, kTraceVoice, _instanceID, "FileRecorder::RecordAudioToFile() recording audio is not turned\ on"); return -1; } AudioFrame tempAudioFrame; tempAudioFrame._payloadDataLengthInSamples = 0; if( incomingAudioFrame._audioChannel == 2 && !_moduleFile->IsStereo()) { // Recording mono but incoming audio is (interleaved) stereo. tempAudioFrame._audioChannel = 1; tempAudioFrame._frequencyInHz = incomingAudioFrame._frequencyInHz; for (WebRtc_UWord16 i = 0; i < (incomingAudioFrame._payloadDataLengthInSamples >> 1); i++) { // Sample value is the average of left and right buffer rounded to // closest integer value. Note samples can be either 1 or 2 byte. tempAudioFrame._payloadData[i] = ((incomingAudioFrame._payloadData[2 * i] + incomingAudioFrame._payloadData[(2 * i) + 1] + 1) >> 1); } tempAudioFrame._payloadDataLengthInSamples = incomingAudioFrame._payloadDataLengthInSamples / 2; } const AudioFrame* ptrAudioFrame = &incomingAudioFrame; if(tempAudioFrame._payloadDataLengthInSamples != 0) { // If ptrAudioFrame is not empty it contains the audio to be recorded. ptrAudioFrame = &tempAudioFrame; } // Encode the audio data before writing to file. Don't encode if the codec // is PCM. // NOTE: stereo recording is only supported for WAV files. // TODO (hellner): WAV expect PCM in little endian byte order. Not // "encoding" with PCM coder should be a problem for big endian systems. WebRtc_UWord32 encodedLenInBytes = 0; if (_fileFormat == kFileFormatPreencodedFile || STR_CASE_CMP(codec_info_.plname, "L16") != 0) { if (_audioEncoder.Encode(*ptrAudioFrame, _audioBuffer, encodedLenInBytes) == -1) { WEBRTC_TRACE( kTraceWarning, kTraceVoice, _instanceID, "FileRecorder::RecordAudioToFile() codec %s not supported or\ failed to encode stream", codec_info_.plname); return -1; } } else { int outLen = 0; if(ptrAudioFrame->_audioChannel == 2) { // ptrAudioFrame contains interleaved stereo audio. _audioResampler.ResetIfNeeded(ptrAudioFrame->_frequencyInHz, codec_info_.plfreq, kResamplerSynchronousStereo); _audioResampler.Push(ptrAudioFrame->_payloadData, ptrAudioFrame->_payloadDataLengthInSamples, (WebRtc_Word16*)_audioBuffer, MAX_AUDIO_BUFFER_IN_BYTES, outLen); } else { _audioResampler.ResetIfNeeded(ptrAudioFrame->_frequencyInHz, codec_info_.plfreq, kResamplerSynchronous); _audioResampler.Push(ptrAudioFrame->_payloadData, ptrAudioFrame->_payloadDataLengthInSamples, (WebRtc_Word16*)_audioBuffer, MAX_AUDIO_BUFFER_IN_BYTES, outLen); } encodedLenInBytes = outLen*2; } // Codec may not be operating at a frame rate of 10 ms. Whenever enough // 10 ms chunks of data has been pushed to the encoder an encoded frame // will be available. Wait until then. if (encodedLenInBytes) { WebRtc_UWord16 msOfData = ptrAudioFrame->_payloadDataLengthInSamples / WebRtc_UWord16(ptrAudioFrame->_frequencyInHz / 1000); if (WriteEncodedAudioData(_audioBuffer, (WebRtc_UWord16)encodedLenInBytes, msOfData, playoutTS) == -1) { return -1; } } return 0; } WebRtc_Word32 FileRecorderImpl::SetUpAudioEncoder() { if (_fileFormat == kFileFormatPreencodedFile || STR_CASE_CMP(codec_info_.plname, "L16") != 0) { if(_audioEncoder.SetEncodeCodec(codec_info_,_amrFormat) == -1) { WEBRTC_TRACE( kTraceError, kTraceVoice, _instanceID, "FileRecorder::StartRecording() codec %s not supported", codec_info_.plname); return -1; } } return 0; } WebRtc_Word32 FileRecorderImpl::codec_info(CodecInst& codecInst) const { if(codec_info_.plfreq == 0) { return -1; } codecInst = codec_info_; return 0; } WebRtc_Word32 FileRecorderImpl::WriteEncodedAudioData( const WebRtc_Word8* audioBuffer, WebRtc_UWord16 bufferLength, WebRtc_UWord16 /*millisecondsOfData*/, const TickTime* /*playoutTS*/) { return _moduleFile->IncomingAudioData(audioBuffer, bufferLength); } #ifdef WEBRTC_MODULE_UTILITY_VIDEO class AudioFrameFileInfo { public: AudioFrameFileInfo(const WebRtc_Word8* audioData, const WebRtc_UWord16 audioSize, const WebRtc_UWord16 audioMS, const TickTime& playoutTS) : _audioSize(audioSize), _audioMS(audioMS) ,_playoutTS(playoutTS) { if(audioSize > MAX_AUDIO_BUFFER_IN_BYTES) { assert(false); _audioSize = 0; return; } memcpy(_audioData, audioData, audioSize); }; // TODO (hellner): either turn into a struct or provide get/set functions. WebRtc_Word8 _audioData[MAX_AUDIO_BUFFER_IN_BYTES]; WebRtc_UWord16 _audioSize; WebRtc_UWord16 _audioMS; TickTime _playoutTS; }; AviRecorder::AviRecorder(WebRtc_UWord32 instanceID, FileFormats fileFormat) : FileRecorderImpl(instanceID, fileFormat), _thread( 0), _videoOnly(false), _timeEvent(*EventWrapper::Create()), _critSec(*CriticalSectionWrapper::CreateCriticalSection()), _writtenVideoFramesCounter(0), _writtenAudioMS(0), _writtenVideoMS(0) { _videoEncoder = new VideoCoder(instanceID); _frameScaler = new FrameScaler(); _videoFramesQueue = new VideoFramesQueue(); _thread = ThreadWrapper::CreateThread(Run, this, kNormalPriority, "AviRecorder()"); } AviRecorder::~AviRecorder( ) { StopRecording( ); delete _videoEncoder; delete _frameScaler; delete _videoFramesQueue; delete _thread; delete &_timeEvent; delete &_critSec; } WebRtc_Word32 AviRecorder::StartRecordingVideoFile( const WebRtc_Word8* fileName, const CodecInst& audioCodecInst, const VideoCodec& videoCodecInst, ACMAMRPackingFormat amrFormat, bool videoOnly) { _firstAudioFrameReceived = false; _videoCodecInst = videoCodecInst; _videoOnly = videoOnly; if(_moduleFile->StartRecordingVideoFile(fileName, _fileFormat, audioCodecInst, videoCodecInst, videoOnly) != 0) { return -1; } if(!videoOnly) { if(FileRecorderImpl::StartRecordingAudioFile(fileName,audioCodecInst, 0, amrFormat) !=0) { StopRecording(); return -1; } } if( SetUpVideoEncoder() != 0) { StopRecording(); return -1; } if(_videoOnly) { // Writing to AVI file is non-blocking. // Start non-blocking timer if video only. If recording both video and // audio let the pushing of audio frames be the timer. _timeEvent.StartTimer(true, 1000 / _videoCodecInst.maxFramerate); } StartThread(); return 0; } WebRtc_Word32 AviRecorder::StopRecording() { _timeEvent.StopTimer(); StopThread(); _videoEncoder->Reset(); return FileRecorderImpl::StopRecording(); } WebRtc_Word32 AviRecorder::CalcI420FrameSize( ) const { return 3 * _videoCodecInst.width * _videoCodecInst.height / 2; } WebRtc_Word32 AviRecorder::SetUpVideoEncoder() { // Size of unencoded data (I420) should be the largest possible frame size // in a file. _videoMaxPayloadSize = CalcI420FrameSize(); _videoEncodedData.VerifyAndAllocate(_videoMaxPayloadSize); _videoCodecInst.plType = _videoEncoder->DefaultPayloadType( _videoCodecInst.plName); WebRtc_Word32 useNumberOfCores = 1; // Set the max payload size to 16000. This means that the codec will try to // create slices that will fit in 16000 kByte packets. However, the // Encode() call will still generate one full frame. if(_videoEncoder->SetEncodeCodec(_videoCodecInst, useNumberOfCores, 16000)) { return -1; } return 0; } WebRtc_Word32 AviRecorder::RecordVideoToFile(const VideoFrame& videoFrame) { CriticalSectionScoped lock(_critSec); if(!IsRecording() || ( videoFrame.Length() == 0)) { return -1; } // The frame is written to file in AviRecorder::Process(). WebRtc_Word32 retVal = _videoFramesQueue->AddFrame(videoFrame); if(retVal != 0) { StopRecording(); } return retVal; } bool AviRecorder::StartThread() { unsigned int id; if( _thread == 0) { return false; } return _thread->Start(id); } bool AviRecorder::StopThread() { _critSec.Enter(); if(_thread) { _thread->SetNotAlive(); ThreadWrapper* thread = _thread; _thread = NULL; _timeEvent.Set(); _critSec.Leave(); if(thread->Stop()) { delete thread; } else { return false; } } else { _critSec.Leave(); } return true; } bool AviRecorder::Run( ThreadObj threadObj) { return static_cast( threadObj)->Process(); } WebRtc_Word32 AviRecorder::ProcessAudio() { if (_writtenVideoFramesCounter == 0) { // Get the most recent frame that is due for writing to file. Since // frames are unencoded it's safe to throw away frames if necessary // for synchronizing audio and video. VideoFrame* frameToProcess = _videoFramesQueue->FrameToRecord(); if(frameToProcess) { // Syncronize audio to the current frame to process by throwing away // audio samples with older timestamp than the video frame. WebRtc_UWord32 numberOfAudioElements = _audioFramesToWrite.GetSize(); for (WebRtc_UWord32 i = 0; i < numberOfAudioElements; ++i) { AudioFrameFileInfo* frameInfo = (AudioFrameFileInfo*)_audioFramesToWrite.First()->GetItem(); if(frameInfo) { if(TickTime::TicksToMilliseconds( frameInfo->_playoutTS.Ticks()) < frameToProcess->RenderTimeMs()) { delete frameInfo; _audioFramesToWrite.PopFront(); } else { break; } } } } } // Write all audio up to current timestamp. WebRtc_Word32 error = 0; WebRtc_UWord32 numberOfAudioElements = _audioFramesToWrite.GetSize(); for (WebRtc_UWord32 i = 0; i < numberOfAudioElements; ++i) { AudioFrameFileInfo* frameInfo = (AudioFrameFileInfo*)_audioFramesToWrite.First()->GetItem(); if(frameInfo) { if((TickTime::Now() - frameInfo->_playoutTS).Milliseconds() > 0) { _moduleFile->IncomingAudioData(frameInfo->_audioData, frameInfo->_audioSize); _writtenAudioMS += frameInfo->_audioMS; delete frameInfo; _audioFramesToWrite.PopFront(); } else { break; } } else { _audioFramesToWrite.PopFront(); } } return error; } bool AviRecorder::Process() { switch(_timeEvent.Wait(500)) { case kEventSignaled: if(_thread == NULL) { return false; } break; case kEventError: return false; case kEventTimeout: // No events triggered. No work to do. return true; } CriticalSectionScoped lock( _critSec); // Get the most recent frame to write to file (if any). Synchronize it with // the audio stream (if any). Synchronization the video based on its render // timestamp (i.e. VideoFrame::RenderTimeMS()) VideoFrame* frameToProcess = _videoFramesQueue->FrameToRecord(); if( frameToProcess == NULL) { return true; } WebRtc_Word32 error = 0; if(!_videoOnly) { if(!_firstAudioFrameReceived) { // Video and audio can only be synchronized if both have been // received. return true; } error = ProcessAudio(); while (_writtenAudioMS > _writtenVideoMS) { error = EncodeAndWriteVideoToFile( *frameToProcess); if( error != 0) { WEBRTC_TRACE(kTraceError, kTraceVideo, _instanceID, "AviRecorder::Process() error writing to file."); break; } else { WebRtc_UWord32 frameLengthMS = 1000 / _videoCodecInst.maxFramerate; _writtenVideoFramesCounter++; _writtenVideoMS += frameLengthMS; // A full seconds worth of frames have been written. if(_writtenVideoFramesCounter%_videoCodecInst.maxFramerate == 0) { // Frame rate is in frames per seconds. Frame length is // calculated as an integer division which means it may // be rounded down. Compensate for this every second. WebRtc_UWord32 rest = 1000 % frameLengthMS; _writtenVideoMS += rest; } } } } else { // Frame rate is in frames per seconds. Frame length is calculated as an // integer division which means it may be rounded down. This introduces // drift. Once a full frame worth of drift has happened, skip writing // one frame. Note that frame rate is in frames per second so the // drift is completely compensated for. WebRtc_UWord32 frameLengthMS = 1000/_videoCodecInst.maxFramerate; WebRtc_UWord32 restMS = 1000 % frameLengthMS; WebRtc_UWord32 frameSkip = (_videoCodecInst.maxFramerate * frameLengthMS) / restMS; _writtenVideoFramesCounter++; if(_writtenVideoFramesCounter % frameSkip == 0) { _writtenVideoMS += frameLengthMS; return true; } error = EncodeAndWriteVideoToFile( *frameToProcess); if(error != 0) { WEBRTC_TRACE(kTraceError, kTraceVideo, _instanceID, "AviRecorder::Process() error writing to file."); } else { _writtenVideoMS += frameLengthMS; } } return error == 0; } WebRtc_Word32 AviRecorder::EncodeAndWriteVideoToFile(VideoFrame& videoFrame) { if(!IsRecording() || (videoFrame.Length() == 0)) { return -1; } if(_frameScaler->ResizeFrameIfNeeded(videoFrame, _videoCodecInst.width, _videoCodecInst.height) != 0) { return -1; } _videoEncodedData.payloadSize = 0; if( STR_CASE_CMP(_videoCodecInst.plName, "I420") == 0) { _videoEncodedData.VerifyAndAllocate(videoFrame.Length()); // I420 is raw data. No encoding needed (each sample is represented by // 1 byte so there is no difference depending on endianness). memcpy(_videoEncodedData.payloadData, videoFrame.Buffer(), videoFrame.Length()); _videoEncodedData.payloadSize = videoFrame.Length(); _videoEncodedData.frameType = kVideoFrameKey; }else { if( _videoEncoder->Encode(videoFrame, _videoEncodedData) != 0) { return -1; } } if(_videoEncodedData.payloadSize > 0) { if(_moduleFile->IncomingAVIVideoData( (WebRtc_Word8*)(_videoEncodedData.payloadData), _videoEncodedData.payloadSize)) { WEBRTC_TRACE(kTraceError, kTraceVideo, _instanceID, "Error writing AVI file"); return -1; } } else { WEBRTC_TRACE( kTraceError, kTraceVideo, _instanceID, "FileRecorder::RecordVideoToFile() frame dropped by encoder bitrate\ likely to low."); } return 0; } // Store audio frame in the _audioFramesToWrite buffer. The writing to file // happens in AviRecorder::Process(). WebRtc_Word32 AviRecorder::WriteEncodedAudioData( const WebRtc_Word8* audioBuffer, WebRtc_UWord16 bufferLength, WebRtc_UWord16 millisecondsOfData, const TickTime* playoutTS) { if (!IsRecording()) { return -1; } if (bufferLength > MAX_AUDIO_BUFFER_IN_BYTES) { return -1; } if (_videoOnly) { return -1; } if (_audioFramesToWrite.GetSize() > kMaxAudioBufferQueueLength) { StopRecording(); return -1; } _firstAudioFrameReceived = true; if(playoutTS) { _audioFramesToWrite.PushBack(new AudioFrameFileInfo(audioBuffer, bufferLength, millisecondsOfData, *playoutTS)); } else { _audioFramesToWrite.PushBack(new AudioFrameFileInfo(audioBuffer, bufferLength, millisecondsOfData, TickTime::Now())); } _timeEvent.Set(); return 0; } #endif // WEBRTC_MODULE_UTILITY_VIDEO } // namespace webrtc