1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
4 * You can obtain one at http://mozilla.org/MPL/2.0/. */
6 #ifndef VideoFrameConverter_h
7 #define VideoFrameConverter_h
9 #include "ImageContainer.h"
10 #include "ImageToI420.h"
12 #include "PerformanceRecorder.h"
13 #include "VideoSegment.h"
14 #include "VideoUtils.h"
15 #include "nsISupportsImpl.h"
16 #include "nsThreadUtils.h"
17 #include "jsapi/RTCStatsReport.h"
18 #include "mozilla/TaskQueue.h"
19 #include "mozilla/dom/ImageBitmapBinding.h"
20 #include "mozilla/dom/ImageUtils.h"
21 #include "api/video/video_frame.h"
22 #include "common_video/include/video_frame_buffer_pool.h"
23 #include "common_video/include/video_frame_buffer.h"
25 // The number of frame buffers VideoFrameConverter may create before returning
27 // Sometimes these are released synchronously but they can be forwarded all the
28 // way to the encoder for asynchronous encoding. With a pool size of 5,
29 // we allow 1 buffer for the current conversion, and 4 buffers to be queued at
31 #define CONVERTER_BUFFER_POOL_SIZE 5
35 static mozilla::LazyLogModule
gVideoFrameConverterLog("VideoFrameConverter");
37 // An async video frame format converter.
39 // Input is typically a MediaTrackListener driven by MediaTrackGraph.
41 // Output is passed through to VideoFrameConvertedEvent() whenever a frame is
43 class VideoFrameConverter
{
45 NS_INLINE_DECL_THREADSAFE_REFCOUNTING(VideoFrameConverter
)
48 explicit VideoFrameConverter(
49 const dom::RTCStatsTimestampMaker
& aTimestampMaker
)
50 : mTimestampMaker(aTimestampMaker
),
51 mTaskQueue(TaskQueue::Create(
52 GetMediaThreadPool(MediaThreadType::WEBRTC_WORKER
),
53 "VideoFrameConverter")),
54 mPacer(MakeAndAddRef
<Pacer
<FrameToProcess
>>(
55 mTaskQueue
, TimeDuration::FromSeconds(1))),
56 mBufferPool(false, CONVERTER_BUFFER_POOL_SIZE
) {
57 MOZ_COUNT_CTOR(VideoFrameConverter
);
60 void RegisterListener() {
61 mPacingListener
= mPacer
->PacedItemEvent().Connect(
63 [self
= RefPtr(this)](FrameToProcess aFrame
, TimeStamp aTime
) {
64 self
->QueueForProcessing(std::move(aFrame
.mImage
), aTime
,
65 aFrame
.mSize
, aFrame
.mForceBlack
);
70 static already_AddRefed
<VideoFrameConverter
> Create(
71 const dom::RTCStatsTimestampMaker
& aTimestampMaker
) {
72 RefPtr
<VideoFrameConverter
> converter
=
73 new VideoFrameConverter(aTimestampMaker
);
74 converter
->RegisterListener();
75 return converter
.forget();
78 void QueueVideoChunk(const VideoChunk
& aChunk
, bool aForceBlack
) {
79 gfx::IntSize size
= aChunk
.mFrame
.GetIntrinsicSize();
80 if (size
.width
== 0 || size
.height
== 0) {
84 TimeStamp t
= aChunk
.mTimeStamp
;
85 MOZ_ASSERT(!t
.IsNull());
88 FrameToProcess(aChunk
.mFrame
.GetImage(), t
, size
, aForceBlack
), t
);
92 * An active VideoFrameConverter actively converts queued video frames.
93 * While inactive, we keep track of the frame most recently queued for
94 * processing, so it can be immediately sent out once activated.
96 void SetActive(bool aActive
) {
97 MOZ_ALWAYS_SUCCEEDS(mTaskQueue
->Dispatch(NS_NewRunnableFunction(
98 __func__
, [self
= RefPtr
<VideoFrameConverter
>(this), this, aActive
,
99 time
= TimeStamp::Now()] {
100 if (mActive
== aActive
) {
103 MOZ_LOG(gVideoFrameConverterLog
, LogLevel::Debug
,
104 ("VideoFrameConverter %p is now %s", this,
105 aActive
? "active" : "inactive"));
107 if (aActive
&& mLastFrameQueuedForProcessing
.Serial() != -2) {
108 // After activating, we re-process the last image that was queued
109 // for processing so it can be immediately sent.
110 mLastFrameQueuedForProcessing
.mTime
= time
;
112 MOZ_ALWAYS_SUCCEEDS(mTaskQueue
->Dispatch(
113 NewRunnableMethod
<StoreCopyPassByLRef
<FrameToProcess
>>(
114 "VideoFrameConverter::ProcessVideoFrame", this,
115 &VideoFrameConverter::ProcessVideoFrame
,
116 mLastFrameQueuedForProcessing
)));
121 void SetTrackEnabled(bool aTrackEnabled
) {
122 MOZ_ALWAYS_SUCCEEDS(mTaskQueue
->Dispatch(NS_NewRunnableFunction(
123 __func__
, [self
= RefPtr
<VideoFrameConverter
>(this), this,
124 aTrackEnabled
, time
= TimeStamp::Now()] {
125 if (mTrackEnabled
== aTrackEnabled
) {
128 MOZ_LOG(gVideoFrameConverterLog
, LogLevel::Debug
,
129 ("VideoFrameConverter %p Track is now %s", this,
130 aTrackEnabled
? "enabled" : "disabled"));
131 mTrackEnabled
= aTrackEnabled
;
132 if (!aTrackEnabled
) {
133 // After disabling we immediately send a frame as black, so it can
134 // be seen quickly, even if no frames are flowing. If no frame has
135 // been queued for processing yet, we use the FrameToProcess default
137 mLastFrameQueuedForProcessing
.mTime
= time
;
138 mLastFrameQueuedForProcessing
.mForceBlack
= true;
139 mLastFrameQueuedForProcessing
.mImage
= nullptr;
141 MOZ_ALWAYS_SUCCEEDS(mTaskQueue
->Dispatch(
142 NewRunnableMethod
<StoreCopyPassByLRef
<FrameToProcess
>>(
143 "VideoFrameConverter::ProcessVideoFrame", this,
144 &VideoFrameConverter::ProcessVideoFrame
,
145 mLastFrameQueuedForProcessing
)));
150 void SetTrackingId(TrackingId aTrackingId
) {
151 MOZ_ALWAYS_SUCCEEDS(mTaskQueue
->Dispatch(NS_NewRunnableFunction(
152 __func__
, [self
= RefPtr
<VideoFrameConverter
>(this), this,
153 id
= std::move(aTrackingId
)]() mutable {
154 mTrackingId
= Some(std::move(id
));
159 mPacer
->Shutdown()->Then(mTaskQueue
, __func__
,
160 [self
= RefPtr
<VideoFrameConverter
>(this), this] {
161 mPacingListener
.DisconnectIfExists();
162 mBufferPool
.Release();
163 mLastFrameQueuedForProcessing
= FrameToProcess();
164 mLastFrameConverted
= Nothing();
168 MediaEventSourceExc
<webrtc::VideoFrame
>& VideoFrameConvertedEvent() {
169 return mVideoFrameConvertedEvent
;
173 struct FrameToProcess
{
174 FrameToProcess() = default;
176 FrameToProcess(RefPtr
<layers::Image
> aImage
, TimeStamp aTime
,
177 gfx::IntSize aSize
, bool aForceBlack
)
178 : mImage(std::move(aImage
)),
181 mForceBlack(aForceBlack
) {}
183 RefPtr
<layers::Image
> mImage
;
184 TimeStamp mTime
= TimeStamp::Now();
185 gfx::IntSize mSize
= gfx::IntSize(640, 480);
186 bool mForceBlack
= false;
188 int32_t Serial() const {
190 // Set the last-img check to indicate black.
191 // -1 is not a guaranteed invalid serial. See bug 1262134.
195 // Set the last-img check to indicate reset.
196 // -2 is not a guaranteed invalid serial. See bug 1262134.
199 return mImage
->GetSerial();
203 struct FrameConverted
{
204 FrameConverted(webrtc::VideoFrame aFrame
, int32_t aSerial
)
205 : mFrame(std::move(aFrame
)), mSerial(aSerial
) {}
207 webrtc::VideoFrame mFrame
;
211 MOZ_COUNTED_DTOR_VIRTUAL(VideoFrameConverter
)
213 void VideoFrameConverted(webrtc::VideoFrame aVideoFrame
, int32_t aSerial
) {
214 MOZ_ASSERT(mTaskQueue
->IsCurrentThreadIn());
217 gVideoFrameConverterLog
, LogLevel::Verbose
,
218 ("VideoFrameConverter %p: Converted a frame. Diff from last: %.3fms",
220 static_cast<double>(aVideoFrame
.timestamp_us() -
222 ? mLastFrameConverted
->mFrame
.timestamp_us()
223 : aVideoFrame
.timestamp_us())) /
226 // Check that time doesn't go backwards
227 MOZ_ASSERT_IF(mLastFrameConverted
,
228 aVideoFrame
.timestamp_us() >
229 mLastFrameConverted
->mFrame
.timestamp_us());
231 mLastFrameConverted
= Some(FrameConverted(aVideoFrame
, aSerial
));
233 mVideoFrameConvertedEvent
.Notify(std::move(aVideoFrame
));
236 void QueueForProcessing(RefPtr
<layers::Image
> aImage
, TimeStamp aTime
,
237 gfx::IntSize aSize
, bool aForceBlack
) {
238 MOZ_ASSERT(mTaskQueue
->IsCurrentThreadIn());
240 FrameToProcess frame
{std::move(aImage
), aTime
, aSize
,
241 aForceBlack
|| !mTrackEnabled
};
243 if (frame
.mTime
<= mLastFrameQueuedForProcessing
.mTime
) {
245 gVideoFrameConverterLog
, LogLevel::Debug
,
246 ("VideoFrameConverter %p: Dropping a frame because time did not "
249 (mLastFrameQueuedForProcessing
.mTime
- frame
.mTime
).ToSeconds()));
253 if (frame
.Serial() == mLastFrameQueuedForProcessing
.Serial()) {
254 // This is the same frame as the last one. We limit the same-frame rate to
255 // 1 second, and rewrite the time so the frame-gap is in whole seconds.
257 // The pacer only starts duplicating frames every second if there is no
258 // flow of frames into it. There are other reasons the same frame could
259 // repeat here, and at a shorter interval than one second. For instance
260 // after the sender is disabled (SetTrackEnabled) but there is still a
261 // flow of frames into the pacer. All disabled frames have the same
263 if (int32_t diffSec
= static_cast<int32_t>(
264 (frame
.mTime
- mLastFrameQueuedForProcessing
.mTime
).ToSeconds());
267 gVideoFrameConverterLog
, LogLevel::Verbose
,
268 ("VideoFrameConverter %p: Rewrote time interval for a duplicate "
269 "frame from %.3fs to %.3fs",
271 (frame
.mTime
- mLastFrameQueuedForProcessing
.mTime
).ToSeconds(),
272 static_cast<float>(diffSec
)));
273 frame
.mTime
= mLastFrameQueuedForProcessing
.mTime
+
274 TimeDuration::FromSeconds(diffSec
);
277 gVideoFrameConverterLog
, LogLevel::Verbose
,
278 ("VideoFrameConverter %p: Dropping a duplicate frame because a "
279 "second hasn't passed (%.3fs)",
281 (frame
.mTime
- mLastFrameQueuedForProcessing
.mTime
).ToSeconds()));
286 mLastFrameQueuedForProcessing
= std::move(frame
);
290 gVideoFrameConverterLog
, LogLevel::Debug
,
291 ("VideoFrameConverter %p: Ignoring a frame because we're inactive",
296 MOZ_ALWAYS_SUCCEEDS(mTaskQueue
->Dispatch(
297 NewRunnableMethod
<StoreCopyPassByLRef
<FrameToProcess
>>(
298 "VideoFrameConverter::ProcessVideoFrame", this,
299 &VideoFrameConverter::ProcessVideoFrame
,
300 mLastFrameQueuedForProcessing
)));
303 void ProcessVideoFrame(const FrameToProcess
& aFrame
) {
304 MOZ_ASSERT(mTaskQueue
->IsCurrentThreadIn());
306 if (aFrame
.mTime
< mLastFrameQueuedForProcessing
.mTime
) {
308 gVideoFrameConverterLog
, LogLevel::Debug
,
309 ("VideoFrameConverter %p: Dropping a frame that is %.3f seconds "
312 (mLastFrameQueuedForProcessing
.mTime
- aFrame
.mTime
).ToSeconds()));
316 const webrtc::Timestamp time
=
317 dom::RTCStatsTimestamp::FromMozTime(mTimestampMaker
, aFrame
.mTime
)
320 if (mLastFrameConverted
&&
321 aFrame
.Serial() == mLastFrameConverted
->mSerial
) {
322 // This is the same input frame as last time. Avoid a conversion.
323 webrtc::VideoFrame frame
= mLastFrameConverted
->mFrame
;
324 frame
.set_timestamp_us(time
.us());
325 VideoFrameConverted(std::move(frame
), mLastFrameConverted
->mSerial
);
329 if (aFrame
.mForceBlack
) {
330 // Send a black image.
331 rtc::scoped_refptr
<webrtc::I420Buffer
> buffer
=
332 mBufferPool
.CreateI420Buffer(aFrame
.mSize
.width
, aFrame
.mSize
.height
);
334 MOZ_DIAGNOSTIC_ASSERT(false,
335 "Buffers not leaving scope except for "
336 "reconfig, should never leak");
337 MOZ_LOG(gVideoFrameConverterLog
, LogLevel::Warning
,
338 ("VideoFrameConverter %p: Creating a buffer for a black video "
344 MOZ_LOG(gVideoFrameConverterLog
, LogLevel::Verbose
,
345 ("VideoFrameConverter %p: Sending a black video frame", this));
346 webrtc::I420Buffer::SetBlack(buffer
.get());
348 VideoFrameConverted(webrtc::VideoFrame::Builder()
349 .set_video_frame_buffer(buffer
)
350 .set_timestamp_us(time
.us())
356 if (!aFrame
.mImage
) {
357 // Don't send anything for null images.
361 MOZ_ASSERT(aFrame
.mImage
->GetSize() == aFrame
.mSize
);
363 RefPtr
<layers::PlanarYCbCrImage
> image
=
364 aFrame
.mImage
->AsPlanarYCbCrImage();
366 dom::ImageUtils
utils(image
);
367 if (utils
.GetFormat() == dom::ImageBitmapFormat::YUV420P
&&
369 const layers::PlanarYCbCrData
* data
= image
->GetData();
370 rtc::scoped_refptr
<webrtc::I420BufferInterface
> video_frame_buffer
=
371 webrtc::WrapI420Buffer(
372 aFrame
.mImage
->GetSize().width
, aFrame
.mImage
->GetSize().height
,
373 data
->mYChannel
, data
->mYStride
, data
->mCbChannel
,
374 data
->mCbCrStride
, data
->mCrChannel
, data
->mCbCrStride
,
375 [image
] { /* keep reference alive*/ });
377 MOZ_LOG(gVideoFrameConverterLog
, LogLevel::Verbose
,
378 ("VideoFrameConverter %p: Sending an I420 video frame", this));
379 VideoFrameConverted(webrtc::VideoFrame::Builder()
380 .set_video_frame_buffer(video_frame_buffer
)
381 .set_timestamp_us(time
.us())
388 rtc::scoped_refptr
<webrtc::I420Buffer
> buffer
=
389 mBufferPool
.CreateI420Buffer(aFrame
.mSize
.width
, aFrame
.mSize
.height
);
391 #ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
394 MOZ_DIAGNOSTIC_ASSERT(mFramesDropped
<= 100, "Buffers must be leaking");
395 MOZ_LOG(gVideoFrameConverterLog
, LogLevel::Warning
,
396 ("VideoFrameConverter %p: Creating a buffer failed", this));
400 #ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
403 PerformanceRecorder
<CopyVideoStage
> rec(
404 "VideoFrameConverter::ConvertToI420"_ns
, *mTrackingId
, buffer
->width(),
407 ConvertToI420(aFrame
.mImage
, buffer
->MutableDataY(), buffer
->StrideY(),
408 buffer
->MutableDataU(), buffer
->StrideU(),
409 buffer
->MutableDataV(), buffer
->StrideV());
412 MOZ_LOG(gVideoFrameConverterLog
, LogLevel::Warning
,
413 ("VideoFrameConverter %p: Image conversion failed", this));
418 VideoFrameConverted(webrtc::VideoFrame::Builder()
419 .set_video_frame_buffer(buffer
)
420 .set_timestamp_us(time
.us())
426 const dom::RTCStatsTimestampMaker mTimestampMaker
;
428 const RefPtr
<TaskQueue
> mTaskQueue
;
431 // Used to pace future frames close to their rendering-time. Thread-safe.
432 const RefPtr
<Pacer
<FrameToProcess
>> mPacer
;
434 MediaEventProducerExc
<webrtc::VideoFrame
> mVideoFrameConvertedEvent
;
436 // Accessed only from mTaskQueue.
437 MediaEventListener mPacingListener
;
438 webrtc::VideoFrameBufferPool mBufferPool
;
439 FrameToProcess mLastFrameQueuedForProcessing
;
440 Maybe
<FrameConverted
> mLastFrameConverted
;
441 bool mActive
= false;
442 bool mTrackEnabled
= true;
443 Maybe
<TrackingId
> mTrackingId
;
444 #ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
445 size_t mFramesDropped
= 0;
449 } // namespace mozilla
451 #endif // VideoFrameConverter_h