blob: f88687150d6f2415162cfbe6d32076f8cc106552 [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;
Sami Kalliomäkiaa35aea2018-05-30 09:24:19 +020032 private final HandlerThread fileThread;
33 private final Handler fileThreadHandler;
mandermo64e1a322016-10-18 08:47:51 -070034 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;
Sami Kalliomäkicd513752018-05-29 13:46:20 +020042 private int frameCount;
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")
Sami Kalliomäkibde473e2017-10-30 13:34:41 +010060 .getBytes(Charset.forName("US-ASCII")));
mandermo64e1a322016-10-18 08:47:51 -070061
Sami Kalliomäkiaa35aea2018-05-30 09:24:19 +020062 renderThread = new HandlerThread(TAG + "RenderThread");
mandermo64e1a322016-10-18 08:47:51 -070063 renderThread.start();
64 renderThreadHandler = new Handler(renderThread.getLooper());
magjed1cb48232016-10-20 03:19:16 -070065
Sami Kalliomäkiaa35aea2018-05-30 09:24:19 +020066 fileThread = new HandlerThread(TAG + "FileThread");
67 fileThread.start();
68 fileThreadHandler = new Handler(fileThread.getLooper());
69
magjed1cb48232016-10-20 03:19:16 -070070 ThreadUtils.invokeAtFrontUninterruptibly(renderThreadHandler, new Runnable() {
71 @Override
72 public void run() {
73 eglBase = EglBase.create(sharedContext, EglBase.CONFIG_PIXEL_BUFFER);
74 eglBase.createDummyPbufferSurface();
75 eglBase.makeCurrent();
76 yuvConverter = new YuvConverter();
77 }
78 });
mandermo64e1a322016-10-18 08:47:51 -070079 }
80
81 @Override
Sami Kalliomäki75db5522018-01-22 14:19:16 +010082 public void onFrame(VideoFrame frame) {
83 frame.retain();
84 renderThreadHandler.post(() -> renderFrameOnRenderThread(frame));
85 }
mandermo64e1a322016-10-18 08:47:51 -070086
Sami Kalliomäki75db5522018-01-22 14:19:16 +010087 private void renderFrameOnRenderThread(VideoFrame frame) {
88 final VideoFrame.Buffer buffer = frame.getBuffer();
mandermo64e1a322016-10-18 08:47:51 -070089
Sami Kalliomäki75db5522018-01-22 14:19:16 +010090 // If the frame is rotated, it will be applied after cropAndScale. Therefore, if the frame is
91 // rotated by 90 degrees, swap width and height.
92 final int targetWidth = frame.getRotation() % 180 == 0 ? outputFileWidth : outputFileHeight;
93 final int targetHeight = frame.getRotation() % 180 == 0 ? outputFileHeight : outputFileWidth;
mandermo64e1a322016-10-18 08:47:51 -070094
Sami Kalliomäki75db5522018-01-22 14:19:16 +010095 final float frameAspectRatio = (float) buffer.getWidth() / (float) buffer.getHeight();
96 final float fileAspectRatio = (float) targetWidth / (float) targetHeight;
mandermo64e1a322016-10-18 08:47:51 -070097
Sami Kalliomäki75db5522018-01-22 14:19:16 +010098 // Calculate cropping to equalize the aspect ratio.
99 int cropWidth = buffer.getWidth();
100 int cropHeight = buffer.getHeight();
101 if (fileAspectRatio > frameAspectRatio) {
Oleh Prypin72467c22018-03-09 10:19:54 +0100102 cropHeight = (int) (cropHeight * (frameAspectRatio / fileAspectRatio));
Sami Kalliomäki75db5522018-01-22 14:19:16 +0100103 } else {
Oleh Prypin72467c22018-03-09 10:19:54 +0100104 cropWidth = (int) (cropWidth * (fileAspectRatio / frameAspectRatio));
mandermo64e1a322016-10-18 08:47:51 -0700105 }
Sami Kalliomäki75db5522018-01-22 14:19:16 +0100106
107 final int cropX = (buffer.getWidth() - cropWidth) / 2;
108 final int cropY = (buffer.getHeight() - cropHeight) / 2;
109
110 final VideoFrame.Buffer scaledBuffer =
111 buffer.cropAndScale(cropX, cropY, cropWidth, cropHeight, targetWidth, targetHeight);
112 frame.release();
113
114 final VideoFrame.I420Buffer i420 = scaledBuffer.toI420();
115 scaledBuffer.release();
116
Sami Kalliomäkiaa35aea2018-05-30 09:24:19 +0200117 fileThreadHandler.post(() -> {
118 YuvHelper.I420Rotate(i420.getDataY(), i420.getStrideY(), i420.getDataU(), i420.getStrideU(),
119 i420.getDataV(), i420.getStrideV(), outputFrameBuffer, i420.getWidth(), i420.getHeight(),
120 frame.getRotation());
121 i420.release();
Sami Kalliomäki75db5522018-01-22 14:19:16 +0100122
Sami Kalliomäkiaa35aea2018-05-30 09:24:19 +0200123 try {
124 videoOutFile.write("FRAME\n".getBytes(Charset.forName("US-ASCII")));
125 videoOutFile.write(
126 outputFrameBuffer.array(), outputFrameBuffer.arrayOffset(), outputFrameSize);
127 } catch (IOException e) {
128 throw new RuntimeException("Error writing video to disk", e);
129 }
130 frameCount++;
131 });
mandermo64e1a322016-10-18 08:47:51 -0700132 }
133
Magnus Jedvert894c4002016-10-21 15:05:01 +0200134 /**
135 * Release all resources. All already posted frames will be rendered first.
136 */
mandermo64e1a322016-10-18 08:47:51 -0700137 public void release() {
Magnus Jedvert894c4002016-10-21 15:05:01 +0200138 final CountDownLatch cleanupBarrier = new CountDownLatch(1);
Sami Kalliomäki75db5522018-01-22 14:19:16 +0100139 renderThreadHandler.post(() -> {
140 yuvConverter.release();
141 eglBase.release();
142 renderThread.quit();
143 cleanupBarrier.countDown();
mandermo64e1a322016-10-18 08:47:51 -0700144 });
Magnus Jedvert894c4002016-10-21 15:05:01 +0200145 ThreadUtils.awaitUninterruptibly(cleanupBarrier);
Sami Kalliomäkiaa35aea2018-05-30 09:24:19 +0200146 fileThreadHandler.post(() -> {
147 try {
148 videoOutFile.close();
149 Logging.d(TAG,
150 "Video written to disk as " + outputFileName + ". The number of frames is " + frameCount
151 + " and the dimensions of the frames are " + outputFileWidth + "x"
152 + outputFileHeight + ".");
153 } catch (IOException e) {
154 throw new RuntimeException("Error closing output file", e);
155 }
156 fileThread.quit();
157 });
mandermoeef94d92017-01-19 09:02:29 -0800158 try {
Sami Kalliomäkiaa35aea2018-05-30 09:24:19 +0200159 fileThread.join();
160 } catch (InterruptedException e) {
161 Thread.currentThread().interrupt();
162 Logging.e(TAG, "Interrupted while waiting for the write to disk to complete.", e);
mandermoeef94d92017-01-19 09:02:29 -0800163 }
mandermo64e1a322016-10-18 08:47:51 -0700164 }
mandermo64e1a322016-10-18 08:47:51 -0700165}