blob: 8c1f7c0e5313b6b856cd3cdf5e8384f36b7b53f5 [file] [log] [blame]
/*
* Copyright (C) 2018-2020 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)
#include "AudioSampleBufferCompressor.h"
#include "AudioStreamDescription.h"
#include "Logging.h"
#include "MediaRecorderPrivate.h"
#include "MediaRecorderPrivateOptions.h"
#include "MediaStreamTrackPrivate.h"
#include "MediaUtilities.h"
#include "VideoSampleBufferCompressor.h"
#include "WebAudioBufferList.h"
#include <AVFoundation/AVAssetWriter.h>
#include <AVFoundation/AVAssetWriterInput.h>
#include <pal/avfoundation/MediaTimeAVFoundation.h>
#include <pal/spi/cocoa/AVAssetWriterSPI.h>
#include <wtf/BlockPtr.h>
#include <wtf/CompletionHandler.h>
#include <wtf/FileSystem.h>
#include <wtf/cf/TypeCastsCF.h>
#include <pal/cf/CoreMediaSoftLink.h>
#include <pal/cocoa/AVFoundationSoftLink.h>
@interface WebAVAssetWriterDelegate : NSObject <AVAssetWriterDelegate> {
WebCore::MediaRecorderPrivateWriter* m_writer;
}
- (instancetype)initWithWriter:(WebCore::MediaRecorderPrivateWriter&)writer;
- (void)close;
@end
@implementation WebAVAssetWriterDelegate {
};
- (instancetype)initWithWriter:(WebCore::MediaRecorderPrivateWriter&)writer
{
ASSERT(isMainThread());
self = [super init];
if (self)
self->m_writer = &writer;
return self;
}
- (void)assetWriter:(AVAssetWriter *)assetWriter didProduceFragmentedHeaderData:(NSData *)fragmentedHeaderData
{
UNUSED_PARAM(assetWriter);
m_writer->appendData(static_cast<const uint8_t*>([fragmentedHeaderData bytes]), [fragmentedHeaderData length]);
}
- (void)assetWriter:(AVAssetWriter *)assetWriter didProduceFragmentedMediaData:(NSData *)fragmentedMediaData fragmentedMediaDataReport:(AVFragmentedMediaDataReport *)fragmentedMediaDataReport
{
UNUSED_PARAM(assetWriter);
UNUSED_PARAM(fragmentedMediaDataReport);
m_writer->appendData(static_cast<const uint8_t*>([fragmentedMediaData bytes]), [fragmentedMediaData length]);
}
- (void)close
{
m_writer = nullptr;
}
@end
namespace WebCore {
RefPtr<MediaRecorderPrivateWriter> MediaRecorderPrivateWriter::create(bool hasAudio, bool hasVideo, const MediaRecorderPrivateOptions& options)
{
auto writer = adoptRef(*new MediaRecorderPrivateWriter(hasAudio, hasVideo));
if (!writer->initialize(options))
return nullptr;
return writer;
}
void MediaRecorderPrivateWriter::compressedVideoOutputBufferCallback(void *mediaRecorderPrivateWriter, CMBufferQueueTriggerToken)
{
callOnMainThread([weakWriter = WeakPtr { static_cast<MediaRecorderPrivateWriter*>(mediaRecorderPrivateWriter) }] {
if (weakWriter)
weakWriter->processNewCompressedVideoSampleBuffers();
});
}
void MediaRecorderPrivateWriter::compressedAudioOutputBufferCallback(void *mediaRecorderPrivateWriter, CMBufferQueueTriggerToken)
{
callOnMainThread([weakWriter = WeakPtr { static_cast<MediaRecorderPrivateWriter*>(mediaRecorderPrivateWriter) }] {
if (weakWriter)
weakWriter->processNewCompressedAudioSampleBuffers();
});
}
MediaRecorderPrivateWriter::MediaRecorderPrivateWriter(bool hasAudio, bool hasVideo)
: m_hasAudio(hasAudio)
, m_hasVideo(hasVideo)
, m_lastVideoPresentationTime(PAL::kCMTimeInvalid)
, m_lastVideoDecodingTime(PAL::kCMTimeInvalid)
, m_resumedVideoTime(PAL::kCMTimeZero)
, m_currentVideoDuration(PAL::kCMTimeZero)
, m_currentAudioSampleTime(PAL::kCMTimeZero)
{
}
MediaRecorderPrivateWriter::~MediaRecorderPrivateWriter()
{
m_pendingAudioSampleQueue.clear();
m_pendingVideoSampleQueue.clear();
if (m_writer) {
[m_writer cancelWriting];
m_writer.clear();
}
// At this pointer, we should no longer be writing any data, so it should be safe to close and nullify m_data without locking.
if (m_writerDelegate)
[m_writerDelegate close];
m_data = nullptr;
if (auto completionHandler = WTFMove(m_fetchDataCompletionHandler))
completionHandler(nullptr, 0);
}
bool MediaRecorderPrivateWriter::initialize(const MediaRecorderPrivateOptions& options)
{
NSError *error = nil;
ALLOW_DEPRECATED_DECLARATIONS_BEGIN
m_writer = adoptNS([PAL::allocAVAssetWriterInstance() initWithFileType:AVFileTypeMPEG4 error:&error]);
ALLOW_DEPRECATED_DECLARATIONS_END
if (error) {
RELEASE_LOG_ERROR(MediaStream, "create AVAssetWriter instance failed with error code %ld", (long)error.code);
return false;
}
m_writerDelegate = adoptNS([[WebAVAssetWriterDelegate alloc] initWithWriter: *this]);
[m_writer setDelegate:m_writerDelegate.get()];
if (m_hasAudio) {
m_audioCompressor = AudioSampleBufferCompressor::create(compressedAudioOutputBufferCallback, this);
if (!m_audioCompressor)
return false;
if (options.audioBitsPerSecond)
m_audioCompressor->setBitsPerSecond(*options.audioBitsPerSecond);
}
if (m_hasVideo) {
m_videoCompressor = VideoSampleBufferCompressor::create(options.mimeType, compressedVideoOutputBufferCallback, this);
if (!m_videoCompressor)
return false;
if (options.videoBitsPerSecond)
m_videoCompressor->setBitsPerSecond(*options.videoBitsPerSecond);
}
return true;
}
void MediaRecorderPrivateWriter::processNewCompressedVideoSampleBuffers()
{
ASSERT(m_hasVideo);
if (!m_videoFormatDescription) {
m_videoFormatDescription = PAL::CMSampleBufferGetFormatDescription(m_videoCompressor->getOutputSampleBuffer());
if (m_hasAudio && !m_audioFormatDescription)
return;
startAssetWriter();
}
if (!m_hasStartedWriting)
return;
appendCompressedSampleBuffers();
}
void MediaRecorderPrivateWriter::processNewCompressedAudioSampleBuffers()
{
ASSERT(m_hasAudio);
if (!m_audioFormatDescription) {
m_audioFormatDescription = PAL::CMSampleBufferGetFormatDescription(m_audioCompressor->getOutputSampleBuffer());
if (m_hasVideo && !m_videoFormatDescription)
return;
startAssetWriter();
}
if (!m_hasStartedWriting)
return;
appendCompressedSampleBuffers();
}
void MediaRecorderPrivateWriter::startAssetWriter()
{
if (m_hasVideo) {
m_videoAssetWriterInput = adoptNS([PAL::allocAVAssetWriterInputInstance() initWithMediaType:AVMediaTypeVideo outputSettings:nil sourceFormatHint:m_videoFormatDescription.get()]);
[m_videoAssetWriterInput setExpectsMediaDataInRealTime:true];
if (m_videoTransform)
m_videoAssetWriterInput.get().transform = *m_videoTransform;
if (![m_writer canAddInput:m_videoAssetWriterInput.get()]) {
RELEASE_LOG_ERROR(MediaStream, "MediaRecorderPrivateWriter::startAssetWriter failed canAddInput for video");
return;
}
[m_writer addInput:m_videoAssetWriterInput.get()];
}
if (m_hasAudio) {
m_audioAssetWriterInput = adoptNS([PAL::allocAVAssetWriterInputInstance() initWithMediaType:AVMediaTypeAudio outputSettings:nil sourceFormatHint:m_audioFormatDescription.get()]);
[m_audioAssetWriterInput setExpectsMediaDataInRealTime:true];
if (![m_writer canAddInput:m_audioAssetWriterInput.get()]) {
RELEASE_LOG_ERROR(MediaStream, "MediaRecorderPrivateWriter::startAssetWriter failed canAddInput for audio");
return;
}
[m_writer addInput:m_audioAssetWriterInput.get()];
}
if (![m_writer startWriting]) {
RELEASE_LOG_ERROR(MediaStream, "MediaRecorderPrivateWriter::startAssetWriter failed startWriting");
return;
}
[m_writer startSessionAtSourceTime:PAL::kCMTimeZero];
appendCompressedSampleBuffers();
m_hasStartedWriting = true;
}
bool MediaRecorderPrivateWriter::appendCompressedAudioSampleBufferIfPossible()
{
if (!m_audioCompressor)
return false;
auto buffer = m_audioCompressor->takeOutputSampleBuffer();
if (!buffer)
return false;
if (m_isFlushingSamples) {
m_pendingAudioSampleQueue.append(WTFMove(buffer));
return true;
}
while (!m_pendingAudioSampleQueue.isEmpty() && [m_audioAssetWriterInput isReadyForMoreMediaData])
[m_audioAssetWriterInput appendSampleBuffer:m_pendingAudioSampleQueue.takeFirst().get()];
if (![m_audioAssetWriterInput isReadyForMoreMediaData]) {
m_pendingAudioSampleQueue.append(WTFMove(buffer));
return true;
}
[m_audioAssetWriterInput appendSampleBuffer:buffer.get()];
return true;
}
bool MediaRecorderPrivateWriter::appendCompressedVideoSampleBufferIfPossible()
{
if (!m_videoCompressor)
return false;
auto buffer = m_videoCompressor->takeOutputSampleBuffer();
if (!buffer)
return false;
if (m_isFlushingSamples) {
m_pendingVideoSampleQueue.append(WTFMove(buffer));
return true;
}
while (!m_pendingVideoSampleQueue.isEmpty() && [m_videoAssetWriterInput isReadyForMoreMediaData])
appendCompressedVideoSampleBuffer(m_pendingVideoSampleQueue.takeFirst().get());
if (![m_videoAssetWriterInput isReadyForMoreMediaData]) {
m_pendingVideoSampleQueue.append(WTFMove(buffer));
return true;
}
appendCompressedVideoSampleBuffer(buffer.get());
return true;
}
void MediaRecorderPrivateWriter::appendCompressedVideoSampleBuffer(CMSampleBufferRef buffer)
{
ASSERT([m_videoAssetWriterInput isReadyForMoreMediaData]);
m_lastVideoPresentationTime = PAL::CMSampleBufferGetPresentationTimeStamp(buffer);
m_lastVideoDecodingTime = PAL::CMSampleBufferGetDecodeTimeStamp(buffer);
m_hasEncodedVideoSamples = true;
[m_videoAssetWriterInput appendSampleBuffer:buffer];
}
void MediaRecorderPrivateWriter::appendCompressedSampleBuffers()
{
while (appendCompressedVideoSampleBufferIfPossible() || appendCompressedAudioSampleBufferIfPossible()) { };
}
static inline void appendEndsPreviousSampleDurationMarker(AVAssetWriterInput *assetWriterInput, CMTime presentationTimeStamp, CMTime decodingTimeStamp)
{
CMSampleTimingInfo timingInfo = { PAL::kCMTimeInvalid, presentationTimeStamp, decodingTimeStamp};
CMSampleBufferRef buffer = NULL;
auto error = PAL::CMSampleBufferCreate(kCFAllocatorDefault, NULL, true, NULL, NULL, NULL, 0, 1, &timingInfo, 0, NULL, &buffer);
if (error) {
RELEASE_LOG_ERROR(MediaStream, "MediaRecorderPrivateWriter appendEndsPreviousSampleDurationMarker failed CMSampleBufferCreate with %d", error);
return;
}
auto sampleBuffer = adoptCF(buffer);
PAL::CMSetAttachment(sampleBuffer.get(), PAL::kCMSampleBufferAttachmentKey_EndsPreviousSampleDuration, kCFBooleanTrue, kCMAttachmentMode_ShouldPropagate);
if (![assetWriterInput appendSampleBuffer:sampleBuffer.get()])
RELEASE_LOG_ERROR(MediaStream, "MediaRecorderPrivateWriter appendSampleBuffer to writer input failed");
}
void MediaRecorderPrivateWriter::flushCompressedSampleBuffers(Function<void()>&& callback)
{
bool hasPendingAudioSamples = !m_pendingAudioSampleQueue.isEmpty();
bool hasPendingVideoSamples = !m_pendingVideoSampleQueue.isEmpty();
if (m_hasEncodedVideoSamples) {
hasPendingVideoSamples |= ![m_videoAssetWriterInput isReadyForMoreMediaData];
if (!hasPendingVideoSamples)
appendEndsPreviousSampleDurationMarker(m_videoAssetWriterInput.get(), m_lastVideoPresentationTime, m_lastVideoDecodingTime);
}
if (!hasPendingAudioSamples && !hasPendingVideoSamples) {
callback();
return;
}
ASSERT(!m_isFlushingSamples);
m_isFlushingSamples = true;
auto block = makeBlockPtr([this, weakThis = WeakPtr { *this }, hasPendingAudioSamples, hasPendingVideoSamples, audioSampleQueue = WTFMove(m_pendingAudioSampleQueue), videoSampleQueue = WTFMove(m_pendingVideoSampleQueue), callback = WTFMove(callback)]() mutable {
if (!weakThis) {
callback();
return;
}
while (!audioSampleQueue.isEmpty() && [m_audioAssetWriterInput isReadyForMoreMediaData])
[m_audioAssetWriterInput appendSampleBuffer:audioSampleQueue.takeFirst().get()];
while (!videoSampleQueue.isEmpty() && [m_videoAssetWriterInput isReadyForMoreMediaData])
appendCompressedVideoSampleBuffer(videoSampleQueue.takeFirst().get());
if (!audioSampleQueue.isEmpty() || !videoSampleQueue.isEmpty() || (hasPendingVideoSamples && ![m_videoAssetWriterInput isReadyForMoreMediaData]))
return;
if (hasPendingAudioSamples)
[m_audioAssetWriterInput markAsFinished];
if (hasPendingVideoSamples) {
appendEndsPreviousSampleDurationMarker(m_videoAssetWriterInput.get(), m_lastVideoPresentationTime, m_lastVideoDecodingTime);
[m_videoAssetWriterInput markAsFinished];
}
m_isFlushingSamples = false;
callback();
finishedFlushingSamples();
});
if (hasPendingAudioSamples)
[m_audioAssetWriterInput requestMediaDataWhenReadyOnQueue:dispatch_get_main_queue() usingBlock:block.get()];
if (hasPendingVideoSamples)
[m_videoAssetWriterInput requestMediaDataWhenReadyOnQueue:dispatch_get_main_queue() usingBlock:block.get()];
}
static inline RetainPtr<CMSampleBufferRef> copySampleBufferWithCurrentTimeStamp(CMSampleBufferRef originalBuffer, CMTime startTime)
{
CMItemCount count = 0;
PAL::CMSampleBufferGetSampleTimingInfoArray(originalBuffer, 0, nil, &count);
Vector<CMSampleTimingInfo> timeInfo(count);
PAL::CMSampleBufferGetSampleTimingInfoArray(originalBuffer, count, timeInfo.data(), &count);
for (auto i = 0; i < count; i++) {
timeInfo[i].decodeTimeStamp = PAL::kCMTimeInvalid;
timeInfo[i].presentationTimeStamp = startTime;
}
CMSampleBufferRef newBuffer = nullptr;
if (auto error = PAL::CMSampleBufferCreateCopyWithNewTiming(kCFAllocatorDefault, originalBuffer, count, timeInfo.data(), &newBuffer)) {
RELEASE_LOG_ERROR(MediaStream, "MediaRecorderPrivateWriter CMSampleBufferCreateCopyWithNewTiming failed with %d", error);
return nullptr;
}
return adoptCF(newBuffer);
}
void MediaRecorderPrivateWriter::appendVideoSampleBuffer(MediaSample& sample)
{
if (!m_firstVideoFrame) {
m_firstVideoFrame = true;
m_resumedVideoTime = PAL::CMClockGetTime(PAL::CMClockGetHostTimeClock());
if (sample.videoRotation() != MediaSample::VideoRotation::None || sample.videoMirrored()) {
m_videoTransform = CGAffineTransformMakeRotation(static_cast<int>(sample.videoRotation()) * M_PI / 180);
if (sample.videoMirrored())
m_videoTransform = CGAffineTransformScale(*m_videoTransform, -1, 1);
}
}
auto sampleTime = PAL::CMTimeSubtract(PAL::CMClockGetTime(PAL::CMClockGetHostTimeClock()), m_resumedVideoTime);
sampleTime = PAL::CMTimeAdd(sampleTime, m_currentVideoDuration);
if (auto bufferWithCurrentTime = copySampleBufferWithCurrentTimeStamp(sample.platformSample().sample.cmSampleBuffer, sampleTime))
m_videoCompressor->addSampleBuffer(bufferWithCurrentTime.get());
}
void MediaRecorderPrivateWriter::appendAudioSampleBuffer(const PlatformAudioData& data, const AudioStreamDescription& description, const WTF::MediaTime&, size_t sampleCount)
{
if (auto sampleBuffer = createAudioSampleBuffer(data, description, m_currentAudioSampleTime, sampleCount))
m_audioCompressor->addSampleBuffer(sampleBuffer.get());
m_currentAudioSampleTime = PAL::CMTimeAdd(m_currentAudioSampleTime, PAL::toCMTime(MediaTime(sampleCount, description.sampleRate())));
}
void MediaRecorderPrivateWriter::finishedFlushingSamples()
{
if (m_shouldStopAfterFlushingSamples)
stopRecording();
}
void MediaRecorderPrivateWriter::stopRecording()
{
if (m_isFlushingSamples) {
m_shouldStopAfterFlushingSamples = true;
return;
}
if (m_isStopped)
return;
m_isStopped = true;
if (m_videoCompressor)
m_videoCompressor->finish();
if (m_audioCompressor)
m_audioCompressor->finish();
m_isStopping = true;
// We hop to the main thread since finishing the video compressor might trigger starting the writer asynchronously.
callOnMainThread([this, weakThis = WeakPtr { *this }]() mutable {
if (!weakThis)
return;
auto whenFinished = [this, weakThis] {
if (!weakThis)
return;
m_isStopping = false;
m_isStopped = false;
m_hasStartedWriting = false;
if (m_writer) {
[m_writer cancelWriting];
m_writer.clear();
}
if (m_fetchDataCompletionHandler)
m_fetchDataCompletionHandler(takeData(), 0);
};
if (!m_hasStartedWriting) {
whenFinished();
return;
}
ASSERT([m_writer status] == AVAssetWriterStatusWriting);
flushCompressedSampleBuffers([this, weakThis = WTFMove(weakThis), whenFinished = WTFMove(whenFinished)]() mutable {
if (!weakThis)
return;
ALLOW_DEPRECATED_DECLARATIONS_BEGIN
[m_writer flush];
ALLOW_DEPRECATED_DECLARATIONS_END
[m_writer finishWritingWithCompletionHandler:[whenFinished = WTFMove(whenFinished)]() mutable {
callOnMainThread(WTFMove(whenFinished));
}];
});
});
}
void MediaRecorderPrivateWriter::fetchData(CompletionHandler<void(RefPtr<SharedBuffer>&&, double)>&& completionHandler)
{
m_fetchDataCompletionHandler = WTFMove(completionHandler);
if (m_isStopping)
return;
if (!m_hasStartedWriting) {
completeFetchData();
return;
}
flushCompressedSampleBuffers([weakThis = WeakPtr { *this }]() mutable {
if (!weakThis)
return;
ALLOW_DEPRECATED_DECLARATIONS_BEGIN
[weakThis->m_writer flush];
ALLOW_DEPRECATED_DECLARATIONS_END
callOnMainThread([weakThis = WTFMove(weakThis)] {
if (weakThis)
weakThis->completeFetchData();
});
});
}
void MediaRecorderPrivateWriter::completeFetchData()
{
auto currentTimeCode = m_timeCode;
if (m_hasAudio)
m_timeCode = PAL::CMTimeGetSeconds(m_currentAudioSampleTime);
else {
auto sampleTime = PAL::CMTimeSubtract(PAL::CMClockGetTime(PAL::CMClockGetHostTimeClock()), m_resumedVideoTime);
m_timeCode = PAL::CMTimeGetSeconds(PAL::CMTimeAdd(sampleTime, m_currentVideoDuration));
}
m_fetchDataCompletionHandler(takeData(), currentTimeCode);
}
void MediaRecorderPrivateWriter::appendData(const uint8_t* data, size_t size)
{
Locker locker { m_dataLock };
if (!m_data) {
m_data = SharedBuffer::create(data, size);
return;
}
m_data->append(data, size);
}
RefPtr<SharedBuffer> MediaRecorderPrivateWriter::takeData()
{
Locker locker { m_dataLock };
auto data = WTFMove(m_data);
return data;
}
void MediaRecorderPrivateWriter::pause()
{
auto recordingDuration = PAL::CMTimeSubtract(PAL::CMClockGetTime(PAL::CMClockGetHostTimeClock()), m_resumedVideoTime);
m_currentVideoDuration = PAL::CMTimeAdd(recordingDuration, m_currentVideoDuration);
}
void MediaRecorderPrivateWriter::resume()
{
m_resumedVideoTime = PAL::CMClockGetTime(PAL::CMClockGetHostTimeClock());
}
const String& MediaRecorderPrivateWriter::mimeType() const
{
static NeverDestroyed<const String> audioMP4(MAKE_STATIC_STRING_IMPL("audio/mp4"));
static NeverDestroyed<const String> videoMP4(MAKE_STATIC_STRING_IMPL("video/mp4"));
// FIXME: we will need to support MIME type codecs parameter values.
return m_hasVideo ? videoMP4 : audioMP4;
}
unsigned MediaRecorderPrivateWriter::audioBitRate() const
{
return m_audioCompressor ? m_audioCompressor->bitRate() : 0;
}
unsigned MediaRecorderPrivateWriter::videoBitRate() const
{
return m_videoCompressor ? m_videoCompressor->bitRate() : 0;
}
} // namespace WebCore
#endif // ENABLE(MEDIA_STREAM)