blob: bb6ebaa1b676db940237213ff1c6663aefb893e2 [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;
mandermoeef94d92017-01-19 09:02:29 -080018import java.util.ArrayList;
Sami Kalliomäkicb98b112017-10-16 11:20:26 +020019import java.util.concurrent.CountDownLatch;
mandermo64e1a322016-10-18 08:47:51 -070020
21/**
22 * Can be used to save the video frames to file.
23 */
24public class VideoFileRenderer implements VideoRenderer.Callbacks {
magjed282d49f2017-04-13 04:17:02 -070025 static {
26 System.loadLibrary("jingle_peerconnection_so");
27 }
28
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;
32 private final Object handlerLock = new Object();
33 private final Handler renderThreadHandler;
34 private final FileOutputStream videoOutFile;
mandermoeef94d92017-01-19 09:02:29 -080035 private final String outputFileName;
mandermo64e1a322016-10-18 08:47:51 -070036 private final int outputFileWidth;
37 private final int outputFileHeight;
38 private final int outputFrameSize;
39 private final ByteBuffer outputFrameBuffer;
magjed1cb48232016-10-20 03:19:16 -070040 private EglBase eglBase;
41 private YuvConverter yuvConverter;
mandermoeef94d92017-01-19 09:02:29 -080042 private ArrayList<ByteBuffer> rawFrames = new ArrayList<>();
mandermo64e1a322016-10-18 08:47:51 -070043
44 public VideoFileRenderer(String outputFile, int outputFileWidth, int outputFileHeight,
magjed1cb48232016-10-20 03:19:16 -070045 final EglBase.Context sharedContext) throws IOException {
mandermo64e1a322016-10-18 08:47:51 -070046 if ((outputFileWidth % 2) == 1 || (outputFileHeight % 2) == 1) {
47 throw new IllegalArgumentException("Does not support uneven width or height");
48 }
mandermo64e1a322016-10-18 08:47:51 -070049
mandermoeef94d92017-01-19 09:02:29 -080050 this.outputFileName = outputFile;
mandermo64e1a322016-10-18 08:47:51 -070051 this.outputFileWidth = outputFileWidth;
52 this.outputFileHeight = outputFileHeight;
53
54 outputFrameSize = outputFileWidth * outputFileHeight * 3 / 2;
55 outputFrameBuffer = ByteBuffer.allocateDirect(outputFrameSize);
56
57 videoOutFile = new FileOutputStream(outputFile);
58 videoOutFile.write(
59 ("YUV4MPEG2 C420 W" + outputFileWidth + " H" + outputFileHeight + " Ip F30:1 A1:1\n")
60 .getBytes());
61
62 renderThread = new HandlerThread(TAG);
63 renderThread.start();
64 renderThreadHandler = new Handler(renderThread.getLooper());
magjed1cb48232016-10-20 03:19:16 -070065
66 ThreadUtils.invokeAtFrontUninterruptibly(renderThreadHandler, new Runnable() {
67 @Override
68 public void run() {
69 eglBase = EglBase.create(sharedContext, EglBase.CONFIG_PIXEL_BUFFER);
70 eglBase.createDummyPbufferSurface();
71 eglBase.makeCurrent();
72 yuvConverter = new YuvConverter();
73 }
74 });
mandermo64e1a322016-10-18 08:47:51 -070075 }
76
77 @Override
78 public void renderFrame(final VideoRenderer.I420Frame frame) {
79 renderThreadHandler.post(new Runnable() {
80 @Override
81 public void run() {
82 renderFrameOnRenderThread(frame);
83 }
84 });
85 }
86
87 private void renderFrameOnRenderThread(VideoRenderer.I420Frame frame) {
88 final float frameAspectRatio = (float) frame.rotatedWidth() / (float) frame.rotatedHeight();
89
90 final float[] rotatedSamplingMatrix =
91 RendererCommon.rotateTextureMatrix(frame.samplingMatrix, frame.rotationDegree);
92 final float[] layoutMatrix = RendererCommon.getLayoutMatrix(
93 false, frameAspectRatio, (float) outputFileWidth / outputFileHeight);
94 final float[] texMatrix = RendererCommon.multiplyMatrices(rotatedSamplingMatrix, layoutMatrix);
95
96 try {
Sami Kalliomäkicb98b112017-10-16 11:20:26 +020097 ByteBuffer buffer = JniCommon.allocateNativeByteBuffer(outputFrameSize);
mandermo64e1a322016-10-18 08:47:51 -070098 if (!frame.yuvFrame) {
99 yuvConverter.convert(outputFrameBuffer, outputFileWidth, outputFileHeight, outputFileWidth,
100 frame.textureId, texMatrix);
101
102 int stride = outputFileWidth;
103 byte[] data = outputFrameBuffer.array();
104 int offset = outputFrameBuffer.arrayOffset();
105
106 // Write Y
mandermoeef94d92017-01-19 09:02:29 -0800107 buffer.put(data, offset, outputFileWidth * outputFileHeight);
mandermo64e1a322016-10-18 08:47:51 -0700108
109 // Write U
110 for (int r = outputFileHeight; r < outputFileHeight * 3 / 2; ++r) {
mandermoeef94d92017-01-19 09:02:29 -0800111 buffer.put(data, offset + r * stride, stride / 2);
mandermo64e1a322016-10-18 08:47:51 -0700112 }
113
114 // Write V
115 for (int r = outputFileHeight; r < outputFileHeight * 3 / 2; ++r) {
mandermoeef94d92017-01-19 09:02:29 -0800116 buffer.put(data, offset + r * stride + stride / 2, stride / 2);
mandermo64e1a322016-10-18 08:47:51 -0700117 }
118 } else {
119 nativeI420Scale(frame.yuvPlanes[0], frame.yuvStrides[0], frame.yuvPlanes[1],
120 frame.yuvStrides[1], frame.yuvPlanes[2], frame.yuvStrides[2], frame.width, frame.height,
121 outputFrameBuffer, outputFileWidth, outputFileHeight);
mandermoeef94d92017-01-19 09:02:29 -0800122
123 buffer.put(outputFrameBuffer.array(), outputFrameBuffer.arrayOffset(), outputFrameSize);
mandermo64e1a322016-10-18 08:47:51 -0700124 }
mandermoeef94d92017-01-19 09:02:29 -0800125 buffer.rewind();
126 rawFrames.add(buffer);
mandermo64e1a322016-10-18 08:47:51 -0700127 } finally {
128 VideoRenderer.renderFrameDone(frame);
129 }
130 }
131
Magnus Jedvert894c4002016-10-21 15:05:01 +0200132 /**
133 * Release all resources. All already posted frames will be rendered first.
134 */
mandermo64e1a322016-10-18 08:47:51 -0700135 public void release() {
Magnus Jedvert894c4002016-10-21 15:05:01 +0200136 final CountDownLatch cleanupBarrier = new CountDownLatch(1);
137 renderThreadHandler.post(new Runnable() {
mandermo64e1a322016-10-18 08:47:51 -0700138 @Override
139 public void run() {
magjed1cb48232016-10-20 03:19:16 -0700140 yuvConverter.release();
141 eglBase.release();
142 renderThread.quit();
Magnus Jedvert894c4002016-10-21 15:05:01 +0200143 cleanupBarrier.countDown();
mandermo64e1a322016-10-18 08:47:51 -0700144 }
145 });
Magnus Jedvert894c4002016-10-21 15:05:01 +0200146 ThreadUtils.awaitUninterruptibly(cleanupBarrier);
mandermoeef94d92017-01-19 09:02:29 -0800147 try {
148 for (ByteBuffer buffer : rawFrames) {
149 videoOutFile.write("FRAME\n".getBytes());
150
151 byte[] data = new byte[outputFrameSize];
152 buffer.get(data);
153
154 videoOutFile.write(data);
155
Sami Kalliomäkicb98b112017-10-16 11:20:26 +0200156 JniCommon.freeNativeByteBuffer(buffer);
mandermoeef94d92017-01-19 09:02:29 -0800157 }
158 videoOutFile.close();
159 Logging.d(TAG, "Video written to disk as " + outputFileName + ". Number frames are "
160 + rawFrames.size() + " and the dimension of the frames are " + outputFileWidth + "x"
161 + outputFileHeight + ".");
162 } catch (IOException e) {
163 Logging.e(TAG, "Error writing video to disk", e);
164 }
mandermo64e1a322016-10-18 08:47:51 -0700165 }
166
167 public static native void nativeI420Scale(ByteBuffer srcY, int strideY, ByteBuffer srcU,
168 int strideU, ByteBuffer srcV, int strideV, int width, int height, ByteBuffer dst,
169 int dstWidth, int dstHeight);
mandermo64e1a322016-10-18 08:47:51 -0700170}