• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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