| /* |
| * Copyright (C) 2018 Apple Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' |
| * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, |
| * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS |
| * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
| * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
| * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
| * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
| * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF |
| * THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| #include "config.h" |
| #include "MediaRecorderPrivateWriterCocoa.h" |
| |
| #if ENABLE(MEDIA_STREAM) && USE(AVFOUNDATION) |
| |
| #include "AudioStreamDescription.h" |
| #include "Logging.h" |
| #include "MediaStreamTrackPrivate.h" |
| #include "WebAudioBufferList.h" |
| #include <AVFoundation/AVAssetWriter.h> |
| #include <AVFoundation/AVAssetWriterInput.h> |
| #include <pal/cf/CoreMediaSoftLink.h> |
| #include <wtf/FileSystem.h> |
| |
| #import <pal/cocoa/AVFoundationSoftLink.h> |
| |
| #undef AVEncoderBitRateKey |
| #define AVEncoderBitRateKey getAVEncoderBitRateKeyWithFallback() |
| #undef AVFormatIDKey |
| #define AVFormatIDKey getAVFormatIDKeyWithFallback() |
| #undef AVNumberOfChannelsKey |
| #define AVNumberOfChannelsKey getAVNumberOfChannelsKeyWithFallback() |
| #undef AVSampleRateKey |
| #define AVSampleRateKey getAVSampleRateKeyWithFallback() |
| |
| namespace WebCore { |
| |
| using namespace PAL; |
| |
| static NSString *getAVFormatIDKeyWithFallback() |
| { |
| if (PAL::canLoad_AVFoundation_AVFormatIDKey()) |
| return PAL::get_AVFoundation_AVFormatIDKey(); |
| |
| RELEASE_LOG_ERROR(Media, "Failed to load AVFormatIDKey"); |
| return @"AVFormatIDKey"; |
| } |
| |
| static NSString *getAVNumberOfChannelsKeyWithFallback() |
| { |
| if (PAL::canLoad_AVFoundation_AVNumberOfChannelsKey()) |
| return PAL::get_AVFoundation_AVNumberOfChannelsKey(); |
| |
| RELEASE_LOG_ERROR(Media, "Failed to load AVNumberOfChannelsKey"); |
| return @"AVNumberOfChannelsKey"; |
| } |
| |
| static NSString *getAVSampleRateKeyWithFallback() |
| { |
| if (PAL::canLoad_AVFoundation_AVSampleRateKey()) |
| return PAL::get_AVFoundation_AVSampleRateKey(); |
| |
| RELEASE_LOG_ERROR(Media, "Failed to load AVSampleRateKey"); |
| return @"AVSampleRateKey"; |
| } |
| |
| static NSString *getAVEncoderBitRateKeyWithFallback() |
| { |
| if (PAL::canLoad_AVFoundation_AVEncoderBitRateKey()) |
| return PAL::get_AVFoundation_AVEncoderBitRateKey(); |
| |
| RELEASE_LOG_ERROR(Media, "Failed to load AVEncoderBitRateKey"); |
| return @"AVEncoderBitRateKey"; |
| } |
| |
| RefPtr<MediaRecorderPrivateWriter> MediaRecorderPrivateWriter::create(const MediaStreamTrackPrivate* audioTrack, const MediaStreamTrackPrivate* videoTrack) |
| { |
| NSString *directory = FileSystem::createTemporaryDirectory(@"videos"); |
| NSString *filename = [NSString stringWithFormat:@"/%lld.mp4", CMClockGetTime(CMClockGetHostTimeClock()).value]; |
| NSString *path = [directory stringByAppendingString:filename]; |
| |
| NSURL *outputURL = [NSURL fileURLWithPath:path]; |
| String filePath = [path UTF8String]; |
| NSError *error = nil; |
| auto avAssetWriter = adoptNS([PAL::allocAVAssetWriterInstance() initWithURL:outputURL fileType:AVFileTypeMPEG4 error:&error]); |
| if (error) { |
| RELEASE_LOG_ERROR(MediaStream, "create AVAssetWriter instance failed with error code %ld", (long)error.code); |
| return nullptr; |
| } |
| |
| auto writer = adoptRef(*new MediaRecorderPrivateWriter(WTFMove(avAssetWriter), WTFMove(filePath))); |
| |
| if (audioTrack && !writer->setAudioInput()) |
| return nullptr; |
| |
| if (videoTrack) { |
| auto& settings = videoTrack->settings(); |
| if (!writer->setVideoInput(settings.width(), settings.height())) |
| return nullptr; |
| } |
| |
| return WTFMove(writer); |
| } |
| |
| MediaRecorderPrivateWriter::MediaRecorderPrivateWriter(RetainPtr<AVAssetWriter>&& avAssetWriter, String&& filePath) |
| : m_writer(WTFMove(avAssetWriter)) |
| , m_path(WTFMove(filePath)) |
| { |
| } |
| |
| MediaRecorderPrivateWriter::~MediaRecorderPrivateWriter() |
| { |
| clear(); |
| } |
| |
| void MediaRecorderPrivateWriter::clear() |
| { |
| if (m_videoInput) { |
| m_videoInput.clear(); |
| dispatch_release(m_videoPullQueue); |
| } |
| if (m_audioInput) { |
| m_audioInput.clear(); |
| dispatch_release(m_audioPullQueue); |
| } |
| if (m_writer) |
| m_writer.clear(); |
| } |
| |
| bool MediaRecorderPrivateWriter::setVideoInput(int width, int height) |
| { |
| ASSERT(!m_videoInput); |
| |
| NSDictionary *compressionProperties = @{ |
| AVVideoAverageBitRateKey : [NSNumber numberWithInt:width * height * 12], |
| AVVideoExpectedSourceFrameRateKey : @(30), |
| AVVideoMaxKeyFrameIntervalKey : @(120), |
| AVVideoProfileLevelKey : AVVideoProfileLevelH264MainAutoLevel |
| }; |
| |
| NSDictionary *videoSettings = @{ |
| AVVideoCodecKey: AVVideoCodecH264, |
| AVVideoWidthKey: [NSNumber numberWithInt:width], |
| AVVideoHeightKey: [NSNumber numberWithInt:height], |
| AVVideoCompressionPropertiesKey: compressionProperties |
| }; |
| |
| m_videoInput = adoptNS([PAL::allocAVAssetWriterInputInstance() initWithMediaType:AVMediaTypeVideo outputSettings:videoSettings sourceFormatHint:nil]); |
| [m_videoInput setExpectsMediaDataInRealTime:true]; |
| |
| if (![m_writer canAddInput:m_videoInput.get()]) { |
| m_videoInput = nullptr; |
| RELEASE_LOG_ERROR(MediaStream, "the video input is not allowed to add to the AVAssetWriter"); |
| return false; |
| } |
| [m_writer addInput:m_videoInput.get()]; |
| m_videoPullQueue = dispatch_queue_create("WebCoreVideoRecordingPullBufferQueue", DISPATCH_QUEUE_SERIAL); |
| return true; |
| } |
| |
| bool MediaRecorderPrivateWriter::setAudioInput() |
| { |
| ASSERT(!m_audioInput); |
| |
| NSDictionary *audioSettings = @{ |
| AVEncoderBitRateKey : @(28000), |
| AVFormatIDKey : @(kAudioFormatMPEG4AAC), |
| AVNumberOfChannelsKey : @(1), |
| AVSampleRateKey : @(22050) |
| }; |
| |
| m_audioInput = adoptNS([PAL::allocAVAssetWriterInputInstance() initWithMediaType:AVMediaTypeAudio outputSettings:audioSettings sourceFormatHint:nil]); |
| [m_audioInput setExpectsMediaDataInRealTime:true]; |
| |
| if (![m_writer canAddInput:m_audioInput.get()]) { |
| m_audioInput = nullptr; |
| RELEASE_LOG_ERROR(MediaStream, "the audio input is not allowed to add to the AVAssetWriter"); |
| return false; |
| } |
| [m_writer addInput:m_audioInput.get()]; |
| m_audioPullQueue = dispatch_queue_create("WebCoreAudioRecordingPullBufferQueue", DISPATCH_QUEUE_SERIAL); |
| return true; |
| } |
| |
| static inline RetainPtr<CMSampleBufferRef> copySampleBufferWithCurrentTimeStamp(CMSampleBufferRef originalBuffer) |
| { |
| CMTime startTime = CMClockGetTime(CMClockGetHostTimeClock()); |
| CMItemCount count = 0; |
| CMSampleBufferGetSampleTimingInfoArray(originalBuffer, 0, nil, &count); |
| |
| Vector<CMSampleTimingInfo> timeInfo(count); |
| CMSampleBufferGetSampleTimingInfoArray(originalBuffer, count, timeInfo.data(), &count); |
| |
| for (CMItemCount i = 0; i < count; i++) { |
| timeInfo[i].decodeTimeStamp = kCMTimeInvalid; |
| timeInfo[i].presentationTimeStamp = startTime; |
| } |
| |
| CMSampleBufferRef newBuffer = nullptr; |
| auto error = CMSampleBufferCreateCopyWithNewTiming(kCFAllocatorDefault, originalBuffer, count, timeInfo.data(), &newBuffer); |
| if (error) |
| return nullptr; |
| return adoptCF(newBuffer); |
| } |
| |
| void MediaRecorderPrivateWriter::appendVideoSampleBuffer(CMSampleBufferRef sampleBuffer) |
| { |
| ASSERT(m_videoInput); |
| if (m_isStopped) |
| return; |
| |
| if (!m_hasStartedWriting) { |
| if (![m_writer startWriting]) { |
| m_isStopped = true; |
| RELEASE_LOG_ERROR(MediaStream, "create AVAssetWriter instance failed with error code %ld", (long)[m_writer error]); |
| return; |
| } |
| [m_writer startSessionAtSourceTime:CMClockGetTime(CMClockGetHostTimeClock())]; |
| m_hasStartedWriting = true; |
| RefPtr<MediaRecorderPrivateWriter> protectedThis = this; |
| [m_videoInput requestMediaDataWhenReadyOnQueue:m_videoPullQueue usingBlock:[this, protectedThis = WTFMove(protectedThis)] { |
| do { |
| if (![m_videoInput isReadyForMoreMediaData]) |
| break; |
| auto locker = holdLock(m_videoLock); |
| if (m_videoBufferPool.isEmpty()) |
| break; |
| auto buffer = m_videoBufferPool.takeFirst(); |
| locker.unlockEarly(); |
| if (![m_videoInput appendSampleBuffer:buffer.get()]) |
| break; |
| } while (true); |
| if (m_isStopped && m_videoBufferPool.isEmpty()) { |
| [m_videoInput markAsFinished]; |
| m_finishWritingVideoSemaphore.signal(); |
| } |
| }]; |
| return; |
| } |
| auto bufferWithCurrentTime = copySampleBufferWithCurrentTimeStamp(sampleBuffer); |
| if (!bufferWithCurrentTime) |
| return; |
| |
| auto locker = holdLock(m_videoLock); |
| m_videoBufferPool.append(WTFMove(bufferWithCurrentTime)); |
| } |
| |
| static inline RetainPtr<CMFormatDescriptionRef> createAudioFormatDescription(const AudioStreamDescription& description) |
| { |
| auto basicDescription = WTF::get<const AudioStreamBasicDescription*>(description.platformDescription().description); |
| CMFormatDescriptionRef format = nullptr; |
| auto error = CMAudioFormatDescriptionCreate(kCFAllocatorDefault, basicDescription, 0, NULL, 0, NULL, NULL, &format); |
| if (error) |
| return nullptr; |
| return adoptCF(format); |
| } |
| |
| static inline RetainPtr<CMSampleBufferRef> createAudioSampleBufferWithPacketDescriptions(CMFormatDescriptionRef format, size_t sampleCount) |
| { |
| CMTime startTime = CMClockGetTime(CMClockGetHostTimeClock()); |
| CMSampleBufferRef sampleBuffer = nullptr; |
| auto error = CMAudioSampleBufferCreateWithPacketDescriptions(kCFAllocatorDefault, NULL, false, NULL, NULL, format, sampleCount, startTime, NULL, &sampleBuffer); |
| if (error) |
| return nullptr; |
| return adoptCF(sampleBuffer); |
| } |
| |
| void MediaRecorderPrivateWriter::appendAudioSampleBuffer(const PlatformAudioData& data, const AudioStreamDescription& description, const WTF::MediaTime&, size_t sampleCount) |
| { |
| ASSERT(m_audioInput); |
| if ((!m_hasStartedWriting && m_videoInput) || m_isStopped) |
| return; |
| auto format = createAudioFormatDescription(description); |
| if (!format) |
| return; |
| if (m_isFirstAudioSample) { |
| if (!m_videoInput) { |
| // audio-only recording. |
| if (![m_writer startWriting]) { |
| m_isStopped = true; |
| return; |
| } |
| [m_writer startSessionAtSourceTime:CMClockGetTime(CMClockGetHostTimeClock())]; |
| m_hasStartedWriting = true; |
| } |
| m_isFirstAudioSample = false; |
| RefPtr<MediaRecorderPrivateWriter> protectedThis = this; |
| [m_audioInput requestMediaDataWhenReadyOnQueue:m_audioPullQueue usingBlock:[this, protectedThis = WTFMove(protectedThis)] { |
| do { |
| if (![m_audioInput isReadyForMoreMediaData]) |
| break; |
| auto locker = holdLock(m_audioLock); |
| if (m_audioBufferPool.isEmpty()) |
| break; |
| auto buffer = m_audioBufferPool.takeFirst(); |
| locker.unlockEarly(); |
| [m_audioInput appendSampleBuffer:buffer.get()]; |
| } while (true); |
| if (m_isStopped && m_audioBufferPool.isEmpty()) { |
| [m_audioInput markAsFinished]; |
| m_finishWritingAudioSemaphore.signal(); |
| } |
| }]; |
| } |
| |
| auto sampleBuffer = createAudioSampleBufferWithPacketDescriptions(format.get(), sampleCount); |
| if (!sampleBuffer) |
| return; |
| auto error = CMSampleBufferSetDataBufferFromAudioBufferList(sampleBuffer.get(), kCFAllocatorDefault, kCFAllocatorDefault, 0, downcast<WebAudioBufferList>(data).list()); |
| if (error) |
| return; |
| |
| auto locker = holdLock(m_audioLock); |
| m_audioBufferPool.append(WTFMove(sampleBuffer)); |
| } |
| |
| void MediaRecorderPrivateWriter::stopRecording() |
| { |
| m_isStopped = true; |
| if (!m_hasStartedWriting) |
| return; |
| ASSERT([m_writer status] == AVAssetWriterStatusWriting); |
| if (m_videoInput) |
| m_finishWritingVideoSemaphore.wait(); |
| |
| if (m_audioInput) |
| m_finishWritingAudioSemaphore.wait(); |
| auto weakPtr = makeWeakPtr(*this); |
| [m_writer finishWritingWithCompletionHandler:[this, weakPtr] { |
| m_finishWritingSemaphore.signal(); |
| callOnMainThread([this, weakPtr] { |
| if (!weakPtr) |
| return; |
| m_isStopped = false; |
| m_hasStartedWriting = false; |
| m_isFirstAudioSample = true; |
| clear(); |
| }); |
| }]; |
| } |
| |
| RefPtr<SharedBuffer> MediaRecorderPrivateWriter::fetchData() |
| { |
| if ((m_path.isEmpty() && !m_isStopped) || !m_hasStartedWriting) |
| return nullptr; |
| |
| m_finishWritingSemaphore.wait(); |
| return SharedBuffer::createWithContentsOfFile(m_path); |
| } |
| |
| } // namespace WebCore |
| |
| #endif // ENABLE(MEDIA_STREAM) && USE(AVFOUNDATION) |