blob: 1b9fa48c32064ca627a17b62b4009fd4a9520dae [file] [log] [blame]
mandermo64e1a322016-10-18 08:47:51 -07001/*
2 * Copyright 2016 The WebRTC Project Authors. All rights reserved.
3 *
4 * Use of this source code is governed by a BSD-style license
5 * that can be found in the LICENSE file in the root of the source
6 * tree. An additional intellectual property rights grant can be found
7 * in the file PATENTS. All contributing project authors may
8 * be found in the AUTHORS file in the root of the source tree.
9 */
sakale1674ef2017-01-11 06:22:56 -080010
mandermo64e1a322016-10-18 08:47:51 -070011package org.webrtc;
12
13import android.os.Handler;
14import android.os.HandlerThread;
mandermo64e1a322016-10-18 08:47:51 -070015import java.io.FileOutputStream;
16import java.io.IOException;
Magnus Jedvert894c4002016-10-21 15:05:01 +020017import java.nio.ByteBuffer;
Sami Kalliomäkibde473e2017-10-30 13:34:41 +010018import java.nio.charset.Charset;
mandermoeef94d92017-01-19 09:02:29 -080019import java.util.ArrayList;
Sami Kalliomäki75db5522018-01-22 14:19:16 +010020import java.util.concurrent.BlockingQueue;
Sami Kalliomäkicb98b112017-10-16 11:20:26 +020021import java.util.concurrent.CountDownLatch;
Sami Kalliomäki75db5522018-01-22 14:19:16 +010022import java.util.concurrent.LinkedBlockingQueue;
mandermo64e1a322016-10-18 08:47:51 -070023
24/**
25 * Can be used to save the video frames to file.
26 */
Magnus Jedvert84d8ae52017-12-20 15:12:10 +010027@JNINamespace("webrtc::jni")
Sami Kalliomäki75db5522018-01-22 14:19:16 +010028public class VideoFileRenderer implements VideoRenderer.Callbacks, VideoSink {
mandermo64e1a322016-10-18 08:47:51 -070029 private static final String TAG = "VideoFileRenderer";
30
mandermo64e1a322016-10-18 08:47:51 -070031 private final HandlerThread renderThread;
mandermo64e1a322016-10-18 08:47:51 -070032 private final Handler renderThreadHandler;
33 private final FileOutputStream videoOutFile;
mandermoeef94d92017-01-19 09:02:29 -080034 private final String outputFileName;
mandermo64e1a322016-10-18 08:47:51 -070035 private final int outputFileWidth;
36 private final int outputFileHeight;
37 private final int outputFrameSize;
38 private final ByteBuffer outputFrameBuffer;
magjed1cb48232016-10-20 03:19:16 -070039 private EglBase eglBase;
40 private YuvConverter yuvConverter;
mandermoeef94d92017-01-19 09:02:29 -080041 private ArrayList<ByteBuffer> rawFrames = new ArrayList<>();
mandermo64e1a322016-10-18 08:47:51 -070042
43 public VideoFileRenderer(String outputFile, int outputFileWidth, int outputFileHeight,
magjed1cb48232016-10-20 03:19:16 -070044 final EglBase.Context sharedContext) throws IOException {
mandermo64e1a322016-10-18 08:47:51 -070045 if ((outputFileWidth % 2) == 1 || (outputFileHeight % 2) == 1) {
46 throw new IllegalArgumentException("Does not support uneven width or height");
47 }
mandermo64e1a322016-10-18 08:47:51 -070048
mandermoeef94d92017-01-19 09:02:29 -080049 this.outputFileName = outputFile;
mandermo64e1a322016-10-18 08:47:51 -070050 this.outputFileWidth = outputFileWidth;
51 this.outputFileHeight = outputFileHeight;
52
53 outputFrameSize = outputFileWidth * outputFileHeight * 3 / 2;
54 outputFrameBuffer = ByteBuffer.allocateDirect(outputFrameSize);
55
56 videoOutFile = new FileOutputStream(outputFile);
57 videoOutFile.write(
58 ("YUV4MPEG2 C420 W" + outputFileWidth + " H" + outputFileHeight + " Ip F30:1 A1:1\n")
Sami Kalliomäkibde473e2017-10-30 13:34:41 +010059 .getBytes(Charset.forName("US-ASCII")));
mandermo64e1a322016-10-18 08:47:51 -070060
61 renderThread = new HandlerThread(TAG);
62 renderThread.start();
63 renderThreadHandler = new Handler(renderThread.getLooper());
magjed1cb48232016-10-20 03:19:16 -070064
65 ThreadUtils.invokeAtFrontUninterruptibly(renderThreadHandler, new Runnable() {
66 @Override
67 public void run() {
68 eglBase = EglBase.create(sharedContext, EglBase.CONFIG_PIXEL_BUFFER);
69 eglBase.createDummyPbufferSurface();
70 eglBase.makeCurrent();
71 yuvConverter = new YuvConverter();
72 }
73 });
mandermo64e1a322016-10-18 08:47:51 -070074 }
75
76 @Override
Sami Kalliomäki75db5522018-01-22 14:19:16 +010077 public void renderFrame(final VideoRenderer.I420Frame i420Frame) {
78 final VideoFrame frame = i420Frame.toVideoFrame();
79 onFrame(frame);
80 frame.release();
mandermo64e1a322016-10-18 08:47:51 -070081 }
82
Sami Kalliomäki75db5522018-01-22 14:19:16 +010083 @Override
84 public void onFrame(VideoFrame frame) {
85 frame.retain();
86 renderThreadHandler.post(() -> renderFrameOnRenderThread(frame));
87 }
mandermo64e1a322016-10-18 08:47:51 -070088
Sami Kalliomäki75db5522018-01-22 14:19:16 +010089 private void renderFrameOnRenderThread(VideoFrame frame) {
90 final VideoFrame.Buffer buffer = frame.getBuffer();
mandermo64e1a322016-10-18 08:47:51 -070091
Sami Kalliomäki75db5522018-01-22 14:19:16 +010092 // If the frame is rotated, it will be applied after cropAndScale. Therefore, if the frame is
93 // rotated by 90 degrees, swap width and height.
94 final int targetWidth = frame.getRotation() % 180 == 0 ? outputFileWidth : outputFileHeight;
95 final int targetHeight = frame.getRotation() % 180 == 0 ? outputFileHeight : outputFileWidth;
mandermo64e1a322016-10-18 08:47:51 -070096
Sami Kalliomäki75db5522018-01-22 14:19:16 +010097 final float frameAspectRatio = (float) buffer.getWidth() / (float) buffer.getHeight();
98 final float fileAspectRatio = (float) targetWidth / (float) targetHeight;
mandermo64e1a322016-10-18 08:47:51 -070099
Sami Kalliomäki75db5522018-01-22 14:19:16 +0100100 // Calculate cropping to equalize the aspect ratio.
101 int cropWidth = buffer.getWidth();
102 int cropHeight = buffer.getHeight();
103 if (fileAspectRatio > frameAspectRatio) {
Oleh Prypin72467c22018-03-09 10:19:54 +0100104 cropHeight = (int) (cropHeight * (frameAspectRatio / fileAspectRatio));
Sami Kalliomäki75db5522018-01-22 14:19:16 +0100105 } else {
Oleh Prypin72467c22018-03-09 10:19:54 +0100106 cropWidth = (int) (cropWidth * (fileAspectRatio / frameAspectRatio));
mandermo64e1a322016-10-18 08:47:51 -0700107 }
Sami Kalliomäki75db5522018-01-22 14:19:16 +0100108
109 final int cropX = (buffer.getWidth() - cropWidth) / 2;
110 final int cropY = (buffer.getHeight() - cropHeight) / 2;
111
112 final VideoFrame.Buffer scaledBuffer =
113 buffer.cropAndScale(cropX, cropY, cropWidth, cropHeight, targetWidth, targetHeight);
114 frame.release();
115
116 final VideoFrame.I420Buffer i420 = scaledBuffer.toI420();
117 scaledBuffer.release();
118
119 ByteBuffer byteBuffer = JniCommon.nativeAllocateByteBuffer(outputFrameSize);
120 YuvHelper.I420Rotate(i420.getDataY(), i420.getStrideY(), i420.getDataU(), i420.getStrideU(),
121 i420.getDataV(), i420.getStrideV(), byteBuffer, i420.getWidth(), i420.getHeight(),
122 frame.getRotation());
123 i420.release();
124
125 byteBuffer.rewind();
126 rawFrames.add(byteBuffer);
mandermo64e1a322016-10-18 08:47:51 -0700127 }
128
Magnus Jedvert894c4002016-10-21 15:05:01 +0200129 /**
130 * Release all resources. All already posted frames will be rendered first.
131 */
mandermo64e1a322016-10-18 08:47:51 -0700132 public void release() {
Magnus Jedvert894c4002016-10-21 15:05:01 +0200133 final CountDownLatch cleanupBarrier = new CountDownLatch(1);
Sami Kalliomäki75db5522018-01-22 14:19:16 +0100134 renderThreadHandler.post(() -> {
135 yuvConverter.release();
136 eglBase.release();
137 renderThread.quit();
138 cleanupBarrier.countDown();
mandermo64e1a322016-10-18 08:47:51 -0700139 });
Magnus Jedvert894c4002016-10-21 15:05:01 +0200140 ThreadUtils.awaitUninterruptibly(cleanupBarrier);
mandermoeef94d92017-01-19 09:02:29 -0800141 try {
142 for (ByteBuffer buffer : rawFrames) {
Sami Kalliomäkibde473e2017-10-30 13:34:41 +0100143 videoOutFile.write("FRAME\n".getBytes(Charset.forName("US-ASCII")));
mandermoeef94d92017-01-19 09:02:29 -0800144
145 byte[] data = new byte[outputFrameSize];
146 buffer.get(data);
147
148 videoOutFile.write(data);
149
Magnus Jedvert84d8ae52017-12-20 15:12:10 +0100150 JniCommon.nativeFreeByteBuffer(buffer);
mandermoeef94d92017-01-19 09:02:29 -0800151 }
152 videoOutFile.close();
Sami Kalliomäki75db5522018-01-22 14:19:16 +0100153 Logging.d(TAG,
154 "Video written to disk as " + outputFileName + ". Number frames are " + rawFrames.size()
155 + " and the dimension of the frames are " + outputFileWidth + "x" + outputFileHeight
156 + ".");
mandermoeef94d92017-01-19 09:02:29 -0800157 } catch (IOException e) {
158 Logging.e(TAG, "Error writing video to disk", e);
159 }
mandermo64e1a322016-10-18 08:47:51 -0700160 }
mandermo64e1a322016-10-18 08:47:51 -0700161}