blob: aef80304593bc6e72a0e737232c9853cc67ef2f7 [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;
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 */
Magnus Jedverte987f2b2018-04-23 16:14:47 +020024public class VideoFileRenderer implements VideoSink {
mandermo64e1a322016-10-18 08:47:51 -070025 private static final String TAG = "VideoFileRenderer";
26
mandermo64e1a322016-10-18 08:47:51 -070027 private final HandlerThread renderThread;
mandermo64e1a322016-10-18 08:47:51 -070028 private final Handler renderThreadHandler;
Sami Kalliomäkiaa35aea2018-05-30 09:24:19 +020029 private final HandlerThread fileThread;
30 private final Handler fileThreadHandler;
mandermo64e1a322016-10-18 08:47:51 -070031 private final FileOutputStream videoOutFile;
mandermoeef94d92017-01-19 09:02:29 -080032 private final String outputFileName;
mandermo64e1a322016-10-18 08:47:51 -070033 private final int outputFileWidth;
34 private final int outputFileHeight;
35 private final int outputFrameSize;
36 private final ByteBuffer outputFrameBuffer;
magjed1cb48232016-10-20 03:19:16 -070037 private EglBase eglBase;
38 private YuvConverter yuvConverter;
Sami Kalliomäkicd513752018-05-29 13:46:20 +020039 private int frameCount;
mandermo64e1a322016-10-18 08:47:51 -070040
41 public VideoFileRenderer(String outputFile, int outputFileWidth, int outputFileHeight,
magjed1cb48232016-10-20 03:19:16 -070042 final EglBase.Context sharedContext) throws IOException {
mandermo64e1a322016-10-18 08:47:51 -070043 if ((outputFileWidth % 2) == 1 || (outputFileHeight % 2) == 1) {
44 throw new IllegalArgumentException("Does not support uneven width or height");
45 }
mandermo64e1a322016-10-18 08:47:51 -070046
mandermoeef94d92017-01-19 09:02:29 -080047 this.outputFileName = outputFile;
mandermo64e1a322016-10-18 08:47:51 -070048 this.outputFileWidth = outputFileWidth;
49 this.outputFileHeight = outputFileHeight;
50
51 outputFrameSize = outputFileWidth * outputFileHeight * 3 / 2;
52 outputFrameBuffer = ByteBuffer.allocateDirect(outputFrameSize);
53
54 videoOutFile = new FileOutputStream(outputFile);
55 videoOutFile.write(
56 ("YUV4MPEG2 C420 W" + outputFileWidth + " H" + outputFileHeight + " Ip F30:1 A1:1\n")
Sami Kalliomäkibde473e2017-10-30 13:34:41 +010057 .getBytes(Charset.forName("US-ASCII")));
mandermo64e1a322016-10-18 08:47:51 -070058
Sami Kalliomäkiaa35aea2018-05-30 09:24:19 +020059 renderThread = new HandlerThread(TAG + "RenderThread");
mandermo64e1a322016-10-18 08:47:51 -070060 renderThread.start();
61 renderThreadHandler = new Handler(renderThread.getLooper());
magjed1cb48232016-10-20 03:19:16 -070062
Sami Kalliomäkiaa35aea2018-05-30 09:24:19 +020063 fileThread = new HandlerThread(TAG + "FileThread");
64 fileThread.start();
65 fileThreadHandler = new Handler(fileThread.getLooper());
66
magjed1cb48232016-10-20 03:19:16 -070067 ThreadUtils.invokeAtFrontUninterruptibly(renderThreadHandler, new Runnable() {
68 @Override
69 public void run() {
70 eglBase = EglBase.create(sharedContext, EglBase.CONFIG_PIXEL_BUFFER);
71 eglBase.createDummyPbufferSurface();
72 eglBase.makeCurrent();
73 yuvConverter = new YuvConverter();
74 }
75 });
mandermo64e1a322016-10-18 08:47:51 -070076 }
77
78 @Override
Sami Kalliomäki75db5522018-01-22 14:19:16 +010079 public void onFrame(VideoFrame frame) {
80 frame.retain();
81 renderThreadHandler.post(() -> renderFrameOnRenderThread(frame));
82 }
mandermo64e1a322016-10-18 08:47:51 -070083
Sami Kalliomäki75db5522018-01-22 14:19:16 +010084 private void renderFrameOnRenderThread(VideoFrame frame) {
85 final VideoFrame.Buffer buffer = frame.getBuffer();
mandermo64e1a322016-10-18 08:47:51 -070086
Sami Kalliomäki75db5522018-01-22 14:19:16 +010087 // If the frame is rotated, it will be applied after cropAndScale. Therefore, if the frame is
88 // rotated by 90 degrees, swap width and height.
89 final int targetWidth = frame.getRotation() % 180 == 0 ? outputFileWidth : outputFileHeight;
90 final int targetHeight = frame.getRotation() % 180 == 0 ? outputFileHeight : outputFileWidth;
mandermo64e1a322016-10-18 08:47:51 -070091
Sami Kalliomäki75db5522018-01-22 14:19:16 +010092 final float frameAspectRatio = (float) buffer.getWidth() / (float) buffer.getHeight();
93 final float fileAspectRatio = (float) targetWidth / (float) targetHeight;
mandermo64e1a322016-10-18 08:47:51 -070094
Sami Kalliomäki75db5522018-01-22 14:19:16 +010095 // Calculate cropping to equalize the aspect ratio.
96 int cropWidth = buffer.getWidth();
97 int cropHeight = buffer.getHeight();
98 if (fileAspectRatio > frameAspectRatio) {
Oleh Prypin72467c22018-03-09 10:19:54 +010099 cropHeight = (int) (cropHeight * (frameAspectRatio / fileAspectRatio));
Sami Kalliomäki75db5522018-01-22 14:19:16 +0100100 } else {
Oleh Prypin72467c22018-03-09 10:19:54 +0100101 cropWidth = (int) (cropWidth * (fileAspectRatio / frameAspectRatio));
mandermo64e1a322016-10-18 08:47:51 -0700102 }
Sami Kalliomäki75db5522018-01-22 14:19:16 +0100103
104 final int cropX = (buffer.getWidth() - cropWidth) / 2;
105 final int cropY = (buffer.getHeight() - cropHeight) / 2;
106
107 final VideoFrame.Buffer scaledBuffer =
108 buffer.cropAndScale(cropX, cropY, cropWidth, cropHeight, targetWidth, targetHeight);
109 frame.release();
110
111 final VideoFrame.I420Buffer i420 = scaledBuffer.toI420();
112 scaledBuffer.release();
113
Sami Kalliomäkiaa35aea2018-05-30 09:24:19 +0200114 fileThreadHandler.post(() -> {
115 YuvHelper.I420Rotate(i420.getDataY(), i420.getStrideY(), i420.getDataU(), i420.getStrideU(),
116 i420.getDataV(), i420.getStrideV(), outputFrameBuffer, i420.getWidth(), i420.getHeight(),
117 frame.getRotation());
118 i420.release();
Sami Kalliomäki75db5522018-01-22 14:19:16 +0100119
Sami Kalliomäkiaa35aea2018-05-30 09:24:19 +0200120 try {
121 videoOutFile.write("FRAME\n".getBytes(Charset.forName("US-ASCII")));
122 videoOutFile.write(
123 outputFrameBuffer.array(), outputFrameBuffer.arrayOffset(), outputFrameSize);
124 } catch (IOException e) {
125 throw new RuntimeException("Error writing video to disk", e);
126 }
127 frameCount++;
128 });
mandermo64e1a322016-10-18 08:47:51 -0700129 }
130
Magnus Jedvert894c4002016-10-21 15:05:01 +0200131 /**
132 * Release all resources. All already posted frames will be rendered first.
133 */
mandermo64e1a322016-10-18 08:47:51 -0700134 public void release() {
Magnus Jedvert894c4002016-10-21 15:05:01 +0200135 final CountDownLatch cleanupBarrier = new CountDownLatch(1);
Sami Kalliomäki75db5522018-01-22 14:19:16 +0100136 renderThreadHandler.post(() -> {
137 yuvConverter.release();
138 eglBase.release();
139 renderThread.quit();
140 cleanupBarrier.countDown();
mandermo64e1a322016-10-18 08:47:51 -0700141 });
Magnus Jedvert894c4002016-10-21 15:05:01 +0200142 ThreadUtils.awaitUninterruptibly(cleanupBarrier);
Sami Kalliomäkiaa35aea2018-05-30 09:24:19 +0200143 fileThreadHandler.post(() -> {
144 try {
145 videoOutFile.close();
146 Logging.d(TAG,
147 "Video written to disk as " + outputFileName + ". The number of frames is " + frameCount
148 + " and the dimensions of the frames are " + outputFileWidth + "x"
149 + outputFileHeight + ".");
150 } catch (IOException e) {
151 throw new RuntimeException("Error closing output file", e);
152 }
153 fileThread.quit();
154 });
mandermoeef94d92017-01-19 09:02:29 -0800155 try {
Sami Kalliomäkiaa35aea2018-05-30 09:24:19 +0200156 fileThread.join();
157 } catch (InterruptedException e) {
158 Thread.currentThread().interrupt();
159 Logging.e(TAG, "Interrupted while waiting for the write to disk to complete.", e);
mandermoeef94d92017-01-19 09:02:29 -0800160 }
mandermo64e1a322016-10-18 08:47:51 -0700161 }
mandermo64e1a322016-10-18 08:47:51 -0700162}