blob: adfd6eec13a23729fb931f92f8e0163eedb4e3db [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 Jedverte987f2b2018-04-23 16:14:47 +020027public class VideoFileRenderer implements VideoSink {
mandermo64e1a322016-10-18 08:47:51 -070028 private static final String TAG = "VideoFileRenderer";
29
mandermo64e1a322016-10-18 08:47:51 -070030 private final HandlerThread renderThread;
mandermo64e1a322016-10-18 08:47:51 -070031 private final Handler renderThreadHandler;
32 private final FileOutputStream videoOutFile;
mandermoeef94d92017-01-19 09:02:29 -080033 private final String outputFileName;
mandermo64e1a322016-10-18 08:47:51 -070034 private final int outputFileWidth;
35 private final int outputFileHeight;
36 private final int outputFrameSize;
37 private final ByteBuffer outputFrameBuffer;
magjed1cb48232016-10-20 03:19:16 -070038 private EglBase eglBase;
39 private YuvConverter yuvConverter;
mandermoeef94d92017-01-19 09:02:29 -080040 private ArrayList<ByteBuffer> rawFrames = new ArrayList<>();
mandermo64e1a322016-10-18 08:47:51 -070041
42 public VideoFileRenderer(String outputFile, int outputFileWidth, int outputFileHeight,
magjed1cb48232016-10-20 03:19:16 -070043 final EglBase.Context sharedContext) throws IOException {
mandermo64e1a322016-10-18 08:47:51 -070044 if ((outputFileWidth % 2) == 1 || (outputFileHeight % 2) == 1) {
45 throw new IllegalArgumentException("Does not support uneven width or height");
46 }
mandermo64e1a322016-10-18 08:47:51 -070047
mandermoeef94d92017-01-19 09:02:29 -080048 this.outputFileName = outputFile;
mandermo64e1a322016-10-18 08:47:51 -070049 this.outputFileWidth = outputFileWidth;
50 this.outputFileHeight = outputFileHeight;
51
52 outputFrameSize = outputFileWidth * outputFileHeight * 3 / 2;
53 outputFrameBuffer = ByteBuffer.allocateDirect(outputFrameSize);
54
55 videoOutFile = new FileOutputStream(outputFile);
56 videoOutFile.write(
57 ("YUV4MPEG2 C420 W" + outputFileWidth + " H" + outputFileHeight + " Ip F30:1 A1:1\n")
Sami Kalliomäkibde473e2017-10-30 13:34:41 +010058 .getBytes(Charset.forName("US-ASCII")));
mandermo64e1a322016-10-18 08:47:51 -070059
60 renderThread = new HandlerThread(TAG);
61 renderThread.start();
62 renderThreadHandler = new Handler(renderThread.getLooper());
magjed1cb48232016-10-20 03:19:16 -070063
64 ThreadUtils.invokeAtFrontUninterruptibly(renderThreadHandler, new Runnable() {
65 @Override
66 public void run() {
67 eglBase = EglBase.create(sharedContext, EglBase.CONFIG_PIXEL_BUFFER);
68 eglBase.createDummyPbufferSurface();
69 eglBase.makeCurrent();
70 yuvConverter = new YuvConverter();
71 }
72 });
mandermo64e1a322016-10-18 08:47:51 -070073 }
74
75 @Override
Sami Kalliomäki75db5522018-01-22 14:19:16 +010076 public void onFrame(VideoFrame frame) {
77 frame.retain();
78 renderThreadHandler.post(() -> renderFrameOnRenderThread(frame));
79 }
mandermo64e1a322016-10-18 08:47:51 -070080
Sami Kalliomäki75db5522018-01-22 14:19:16 +010081 private void renderFrameOnRenderThread(VideoFrame frame) {
82 final VideoFrame.Buffer buffer = frame.getBuffer();
mandermo64e1a322016-10-18 08:47:51 -070083
Sami Kalliomäki75db5522018-01-22 14:19:16 +010084 // If the frame is rotated, it will be applied after cropAndScale. Therefore, if the frame is
85 // rotated by 90 degrees, swap width and height.
86 final int targetWidth = frame.getRotation() % 180 == 0 ? outputFileWidth : outputFileHeight;
87 final int targetHeight = frame.getRotation() % 180 == 0 ? outputFileHeight : outputFileWidth;
mandermo64e1a322016-10-18 08:47:51 -070088
Sami Kalliomäki75db5522018-01-22 14:19:16 +010089 final float frameAspectRatio = (float) buffer.getWidth() / (float) buffer.getHeight();
90 final float fileAspectRatio = (float) targetWidth / (float) targetHeight;
mandermo64e1a322016-10-18 08:47:51 -070091
Sami Kalliomäki75db5522018-01-22 14:19:16 +010092 // Calculate cropping to equalize the aspect ratio.
93 int cropWidth = buffer.getWidth();
94 int cropHeight = buffer.getHeight();
95 if (fileAspectRatio > frameAspectRatio) {
Oleh Prypin72467c22018-03-09 10:19:54 +010096 cropHeight = (int) (cropHeight * (frameAspectRatio / fileAspectRatio));
Sami Kalliomäki75db5522018-01-22 14:19:16 +010097 } else {
Oleh Prypin72467c22018-03-09 10:19:54 +010098 cropWidth = (int) (cropWidth * (fileAspectRatio / frameAspectRatio));
mandermo64e1a322016-10-18 08:47:51 -070099 }
Sami Kalliomäki75db5522018-01-22 14:19:16 +0100100
101 final int cropX = (buffer.getWidth() - cropWidth) / 2;
102 final int cropY = (buffer.getHeight() - cropHeight) / 2;
103
104 final VideoFrame.Buffer scaledBuffer =
105 buffer.cropAndScale(cropX, cropY, cropWidth, cropHeight, targetWidth, targetHeight);
106 frame.release();
107
108 final VideoFrame.I420Buffer i420 = scaledBuffer.toI420();
109 scaledBuffer.release();
110
111 ByteBuffer byteBuffer = JniCommon.nativeAllocateByteBuffer(outputFrameSize);
112 YuvHelper.I420Rotate(i420.getDataY(), i420.getStrideY(), i420.getDataU(), i420.getStrideU(),
113 i420.getDataV(), i420.getStrideV(), byteBuffer, i420.getWidth(), i420.getHeight(),
114 frame.getRotation());
115 i420.release();
116
117 byteBuffer.rewind();
118 rawFrames.add(byteBuffer);
mandermo64e1a322016-10-18 08:47:51 -0700119 }
120
Magnus Jedvert894c4002016-10-21 15:05:01 +0200121 /**
122 * Release all resources. All already posted frames will be rendered first.
123 */
mandermo64e1a322016-10-18 08:47:51 -0700124 public void release() {
Magnus Jedvert894c4002016-10-21 15:05:01 +0200125 final CountDownLatch cleanupBarrier = new CountDownLatch(1);
Sami Kalliomäki75db5522018-01-22 14:19:16 +0100126 renderThreadHandler.post(() -> {
127 yuvConverter.release();
128 eglBase.release();
129 renderThread.quit();
130 cleanupBarrier.countDown();
mandermo64e1a322016-10-18 08:47:51 -0700131 });
Magnus Jedvert894c4002016-10-21 15:05:01 +0200132 ThreadUtils.awaitUninterruptibly(cleanupBarrier);
mandermoeef94d92017-01-19 09:02:29 -0800133 try {
134 for (ByteBuffer buffer : rawFrames) {
Sami Kalliomäkibde473e2017-10-30 13:34:41 +0100135 videoOutFile.write("FRAME\n".getBytes(Charset.forName("US-ASCII")));
mandermoeef94d92017-01-19 09:02:29 -0800136
137 byte[] data = new byte[outputFrameSize];
138 buffer.get(data);
139
140 videoOutFile.write(data);
141
Magnus Jedvert84d8ae52017-12-20 15:12:10 +0100142 JniCommon.nativeFreeByteBuffer(buffer);
mandermoeef94d92017-01-19 09:02:29 -0800143 }
144 videoOutFile.close();
Sami Kalliomäki75db5522018-01-22 14:19:16 +0100145 Logging.d(TAG,
146 "Video written to disk as " + outputFileName + ". Number frames are " + rawFrames.size()
147 + " and the dimension of the frames are " + outputFileWidth + "x" + outputFileHeight
148 + ".");
mandermoeef94d92017-01-19 09:02:29 -0800149 } catch (IOException e) {
150 Logging.e(TAG, "Error writing video to disk", e);
151 }
mandermo64e1a322016-10-18 08:47:51 -0700152 }
mandermo64e1a322016-10-18 08:47:51 -0700153}