blob: 4efed36a5f8d7ecf30af2770ca92f92520090c68 [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 */
10
11package org.webrtc;
12
13import android.content.Context;
14import android.os.SystemClock;
Sami Kalliomäki07c5bfb2017-10-05 13:37:18 +020015import java.io.IOException;
16import java.io.RandomAccessFile;
17import java.nio.ByteBuffer;
Sami Kalliomäki06110652018-02-20 11:19:58 +010018import java.nio.channels.FileChannel;
Sami Kalliomäkibde473e2017-10-30 13:34:41 +010019import java.nio.charset.Charset;
mandermo64e1a322016-10-18 08:47:51 -070020import java.util.Timer;
21import java.util.TimerTask;
Sami Kalliomäki07c5bfb2017-10-05 13:37:18 +020022import java.util.concurrent.TimeUnit;
mandermo64e1a322016-10-18 08:47:51 -070023
24public class FileVideoCapturer implements VideoCapturer {
25 private interface VideoReader {
Sami Kalliomäki07c5bfb2017-10-05 13:37:18 +020026 VideoFrame getNextFrame();
mandermo64e1a322016-10-18 08:47:51 -070027 void close();
28 }
29
30 /**
31 * Read video data from file for the .y4m container.
32 */
Oleh Prypine78824e2018-01-15 20:30:59 +010033 @SuppressWarnings("StringSplitter")
mandermo64e1a322016-10-18 08:47:51 -070034 private static class VideoReaderY4M implements VideoReader {
Sami Kalliomäki07c5bfb2017-10-05 13:37:18 +020035 private static final String TAG = "VideoReaderY4M";
mandermo64e1a322016-10-18 08:47:51 -070036 private static final String Y4M_FRAME_DELIMETER = "FRAME";
Sami Kalliomäki06110652018-02-20 11:19:58 +010037 private static final int FRAME_DELIMETER_LENGTH = Y4M_FRAME_DELIMETER.length() + 1;
mandermo64e1a322016-10-18 08:47:51 -070038
Sami Kalliomäki07c5bfb2017-10-05 13:37:18 +020039 private final int frameWidth;
40 private final int frameHeight;
41 // First char after header
42 private final long videoStart;
Sami Kalliomäki06110652018-02-20 11:19:58 +010043 private final RandomAccessFile mediaFile;
44 private final FileChannel mediaFileChannel;
mandermo64e1a322016-10-18 08:47:51 -070045
mandermo64e1a322016-10-18 08:47:51 -070046 public VideoReaderY4M(String file) throws IOException {
Sami Kalliomäki06110652018-02-20 11:19:58 +010047 mediaFile = new RandomAccessFile(file, "r");
48 mediaFileChannel = mediaFile.getChannel();
mandermo64e1a322016-10-18 08:47:51 -070049 StringBuilder builder = new StringBuilder();
50 for (;;) {
Sami Kalliomäki06110652018-02-20 11:19:58 +010051 int c = mediaFile.read();
mandermo64e1a322016-10-18 08:47:51 -070052 if (c == -1) {
53 // End of file reached.
54 throw new RuntimeException("Found end of file before end of header for file: " + file);
55 }
56 if (c == '\n') {
57 // End of header found.
58 break;
59 }
60 builder.append((char) c);
61 }
Sami Kalliomäki06110652018-02-20 11:19:58 +010062 videoStart = mediaFileChannel.position();
mandermo64e1a322016-10-18 08:47:51 -070063 String header = builder.toString();
64 String[] headerTokens = header.split("[ ]");
65 int w = 0;
66 int h = 0;
67 String colorSpace = "";
68 for (String tok : headerTokens) {
69 char c = tok.charAt(0);
70 switch (c) {
71 case 'W':
72 w = Integer.parseInt(tok.substring(1));
73 break;
74 case 'H':
75 h = Integer.parseInt(tok.substring(1));
76 break;
77 case 'C':
78 colorSpace = tok.substring(1);
79 break;
80 }
81 }
82 Logging.d(TAG, "Color space: " + colorSpace);
mandermod192dce2016-11-02 09:15:41 -070083 if (!colorSpace.equals("420") && !colorSpace.equals("420mpeg2")) {
84 throw new IllegalArgumentException(
85 "Does not support any other color space than I420 or I420mpeg2");
mandermo64e1a322016-10-18 08:47:51 -070086 }
87 if ((w % 2) == 1 || (h % 2) == 1) {
88 throw new IllegalArgumentException("Does not support odd width or height");
89 }
90 frameWidth = w;
91 frameHeight = h;
Sami Kalliomäki07c5bfb2017-10-05 13:37:18 +020092 Logging.d(TAG, "frame dim: (" + w + ", " + h + ")");
mandermo64e1a322016-10-18 08:47:51 -070093 }
94
Sami Kalliomäkibde473e2017-10-30 13:34:41 +010095 @Override
Sami Kalliomäki07c5bfb2017-10-05 13:37:18 +020096 public VideoFrame getNextFrame() {
97 final long captureTimeNs = TimeUnit.MILLISECONDS.toNanos(SystemClock.elapsedRealtime());
98 final JavaI420Buffer buffer = JavaI420Buffer.allocate(frameWidth, frameHeight);
99 final ByteBuffer dataY = buffer.getDataY();
100 final ByteBuffer dataU = buffer.getDataU();
101 final ByteBuffer dataV = buffer.getDataV();
102 final int chromaHeight = (frameHeight + 1) / 2;
103 final int sizeY = frameHeight * buffer.getStrideY();
104 final int sizeU = chromaHeight * buffer.getStrideU();
105 final int sizeV = chromaHeight * buffer.getStrideV();
106
mandermo64e1a322016-10-18 08:47:51 -0700107 try {
Sami Kalliomäki06110652018-02-20 11:19:58 +0100108 ByteBuffer frameDelim = ByteBuffer.allocate(FRAME_DELIMETER_LENGTH);
109 if (mediaFileChannel.read(frameDelim) < FRAME_DELIMETER_LENGTH) {
mandermo64e1a322016-10-18 08:47:51 -0700110 // We reach end of file, loop
Sami Kalliomäki06110652018-02-20 11:19:58 +0100111 mediaFileChannel.position(videoStart);
112 if (mediaFileChannel.read(frameDelim) < FRAME_DELIMETER_LENGTH) {
mandermo64e1a322016-10-18 08:47:51 -0700113 throw new RuntimeException("Error looping video");
114 }
115 }
Sami Kalliomäki06110652018-02-20 11:19:58 +0100116 String frameDelimStr = new String(frameDelim.array(), Charset.forName("US-ASCII"));
mandermo64e1a322016-10-18 08:47:51 -0700117 if (!frameDelimStr.equals(Y4M_FRAME_DELIMETER + "\n")) {
118 throw new RuntimeException(
119 "Frames should be delimited by FRAME plus newline, found delimter was: '"
120 + frameDelimStr + "'");
121 }
Sami Kalliomäki07c5bfb2017-10-05 13:37:18 +0200122
Sami Kalliomäki06110652018-02-20 11:19:58 +0100123 mediaFileChannel.read(dataY);
124 mediaFileChannel.read(dataU);
125 mediaFileChannel.read(dataV);
mandermo64e1a322016-10-18 08:47:51 -0700126 } catch (IOException e) {
127 throw new RuntimeException(e);
128 }
Sami Kalliomäki07c5bfb2017-10-05 13:37:18 +0200129
130 return new VideoFrame(buffer, 0 /* rotation */, captureTimeNs);
mandermo64e1a322016-10-18 08:47:51 -0700131 }
132
Sami Kalliomäkibde473e2017-10-30 13:34:41 +0100133 @Override
mandermo64e1a322016-10-18 08:47:51 -0700134 public void close() {
135 try {
Sami Kalliomäki06110652018-02-20 11:19:58 +0100136 // Closing a file also closes the channel.
137 mediaFile.close();
mandermo64e1a322016-10-18 08:47:51 -0700138 } catch (IOException e) {
139 Logging.e(TAG, "Problem closing file", e);
140 }
141 }
142 }
143
144 private final static String TAG = "FileVideoCapturer";
145 private final VideoReader videoReader;
146 private CapturerObserver capturerObserver;
147 private final Timer timer = new Timer();
148
149 private final TimerTask tickTask = new TimerTask() {
150 @Override
151 public void run() {
152 tick();
153 }
154 };
155
mandermo64e1a322016-10-18 08:47:51 -0700156 public FileVideoCapturer(String inputFile) throws IOException {
157 try {
158 videoReader = new VideoReaderY4M(inputFile);
159 } catch (IOException e) {
160 Logging.d(TAG, "Could not open video file: " + inputFile);
161 throw e;
162 }
163 }
164
mandermo64e1a322016-10-18 08:47:51 -0700165 public void tick() {
Sami Kalliomäki07c5bfb2017-10-05 13:37:18 +0200166 capturerObserver.onFrameCaptured(videoReader.getNextFrame());
mandermo64e1a322016-10-18 08:47:51 -0700167 }
168
169 @Override
170 public void initialize(SurfaceTextureHelper surfaceTextureHelper, Context applicationContext,
171 CapturerObserver capturerObserver) {
172 this.capturerObserver = capturerObserver;
173 }
174
175 @Override
176 public void startCapture(int width, int height, int framerate) {
177 timer.schedule(tickTask, 0, 1000 / framerate);
178 }
179
180 @Override
181 public void stopCapture() throws InterruptedException {
182 timer.cancel();
183 }
184
185 @Override
186 public void changeCaptureFormat(int width, int height, int framerate) {
187 // Empty on purpose
188 }
189
190 @Override
191 public void dispose() {
192 videoReader.close();
193 }
194
195 @Override
196 public boolean isScreencast() {
197 return false;
198 }
mandermo64e1a322016-10-18 08:47:51 -0700199}