1 /* 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 */ 10 11 package org.webrtc; 12 13 import android.os.Handler; 14 import android.os.HandlerThread; 15 import java.io.FileOutputStream; 16 import java.io.IOException; 17 import java.nio.ByteBuffer; 18 import java.nio.charset.Charset; 19 import java.util.concurrent.CountDownLatch; 20 21 /** 22 * Can be used to save the video frames to file. 23 */ 24 public class VideoFileRenderer implements VideoSink { 25 private static final String TAG = "VideoFileRenderer"; 26 27 private final HandlerThread renderThread; 28 private final Handler renderThreadHandler; 29 private final HandlerThread fileThread; 30 private final Handler fileThreadHandler; 31 private final FileOutputStream videoOutFile; 32 private final String outputFileName; 33 private final int outputFileWidth; 34 private final int outputFileHeight; 35 private final int outputFrameSize; 36 private final ByteBuffer outputFrameBuffer; 37 private EglBase eglBase; 38 private YuvConverter yuvConverter; 39 private int frameCount; 40 VideoFileRenderer(String outputFile, int outputFileWidth, int outputFileHeight, final EglBase.Context sharedContext)41 public VideoFileRenderer(String outputFile, int outputFileWidth, int outputFileHeight, 42 final EglBase.Context sharedContext) throws IOException { 43 if ((outputFileWidth % 2) == 1 || (outputFileHeight % 2) == 1) { 44 throw new IllegalArgumentException("Does not support uneven width or height"); 45 } 46 47 this.outputFileName = outputFile; 48 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") 57 .getBytes(Charset.forName("US-ASCII"))); 58 59 renderThread = new HandlerThread(TAG + "RenderThread"); 60 renderThread.start(); 61 renderThreadHandler = new Handler(renderThread.getLooper()); 62 63 fileThread = new HandlerThread(TAG + "FileThread"); 64 fileThread.start(); 65 fileThreadHandler = new Handler(fileThread.getLooper()); 66 67 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 }); 76 } 77 78 @Override onFrame(VideoFrame frame)79 public void onFrame(VideoFrame frame) { 80 frame.retain(); 81 renderThreadHandler.post(() -> renderFrameOnRenderThread(frame)); 82 } 83 renderFrameOnRenderThread(VideoFrame frame)84 private void renderFrameOnRenderThread(VideoFrame frame) { 85 final VideoFrame.Buffer buffer = frame.getBuffer(); 86 87 // 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; 91 92 final float frameAspectRatio = (float) buffer.getWidth() / (float) buffer.getHeight(); 93 final float fileAspectRatio = (float) targetWidth / (float) targetHeight; 94 95 // Calculate cropping to equalize the aspect ratio. 96 int cropWidth = buffer.getWidth(); 97 int cropHeight = buffer.getHeight(); 98 if (fileAspectRatio > frameAspectRatio) { 99 cropHeight = (int) (cropHeight * (frameAspectRatio / fileAspectRatio)); 100 } else { 101 cropWidth = (int) (cropWidth * (fileAspectRatio / frameAspectRatio)); 102 } 103 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 114 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(); 119 120 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 }); 129 } 130 131 /** 132 * Release all resources. All already posted frames will be rendered first. 133 */ release()134 public void release() { 135 final CountDownLatch cleanupBarrier = new CountDownLatch(1); 136 renderThreadHandler.post(() -> { 137 yuvConverter.release(); 138 eglBase.release(); 139 renderThread.quit(); 140 cleanupBarrier.countDown(); 141 }); 142 ThreadUtils.awaitUninterruptibly(cleanupBarrier); 143 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 }); 155 try { 156 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); 160 } 161 } 162 } 163