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.content.Context; 14 import android.os.SystemClock; 15 import java.io.IOException; 16 import java.io.RandomAccessFile; 17 import java.nio.ByteBuffer; 18 import java.nio.channels.FileChannel; 19 import java.nio.charset.Charset; 20 import java.util.Timer; 21 import java.util.TimerTask; 22 import java.util.concurrent.TimeUnit; 23 24 public class FileVideoCapturer implements VideoCapturer { 25 private interface VideoReader { getNextFrame()26 VideoFrame getNextFrame(); close()27 void close(); 28 } 29 30 /** 31 * Read video data from file for the .y4m container. 32 */ 33 @SuppressWarnings("StringSplitter") 34 private static class VideoReaderY4M implements VideoReader { 35 private static final String TAG = "VideoReaderY4M"; 36 private static final String Y4M_FRAME_DELIMETER = "FRAME"; 37 private static final int FRAME_DELIMETER_LENGTH = Y4M_FRAME_DELIMETER.length() + 1; 38 39 private final int frameWidth; 40 private final int frameHeight; 41 // First char after header 42 private final long videoStart; 43 private final RandomAccessFile mediaFile; 44 private final FileChannel mediaFileChannel; 45 VideoReaderY4M(String file)46 public VideoReaderY4M(String file) throws IOException { 47 mediaFile = new RandomAccessFile(file, "r"); 48 mediaFileChannel = mediaFile.getChannel(); 49 StringBuilder builder = new StringBuilder(); 50 for (;;) { 51 int c = mediaFile.read(); 52 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 } 62 videoStart = mediaFileChannel.position(); 63 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); 83 if (!colorSpace.equals("420") && !colorSpace.equals("420mpeg2")) { 84 throw new IllegalArgumentException( 85 "Does not support any other color space than I420 or I420mpeg2"); 86 } 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; 92 Logging.d(TAG, "frame dim: (" + w + ", " + h + ")"); 93 } 94 95 @Override getNextFrame()96 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 107 try { 108 ByteBuffer frameDelim = ByteBuffer.allocate(FRAME_DELIMETER_LENGTH); 109 if (mediaFileChannel.read(frameDelim) < FRAME_DELIMETER_LENGTH) { 110 // We reach end of file, loop 111 mediaFileChannel.position(videoStart); 112 if (mediaFileChannel.read(frameDelim) < FRAME_DELIMETER_LENGTH) { 113 throw new RuntimeException("Error looping video"); 114 } 115 } 116 String frameDelimStr = new String(frameDelim.array(), Charset.forName("US-ASCII")); 117 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 } 122 123 mediaFileChannel.read(dataY); 124 mediaFileChannel.read(dataU); 125 mediaFileChannel.read(dataV); 126 } catch (IOException e) { 127 throw new RuntimeException(e); 128 } 129 130 return new VideoFrame(buffer, 0 /* rotation */, captureTimeNs); 131 } 132 133 @Override close()134 public void close() { 135 try { 136 // Closing a file also closes the channel. 137 mediaFile.close(); 138 } 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 FileVideoCapturer(String inputFile)156 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 tick()165 public void tick() { 166 VideoFrame videoFrame = videoReader.getNextFrame(); 167 capturerObserver.onFrameCaptured(videoFrame); 168 videoFrame.release(); 169 } 170 171 @Override initialize(SurfaceTextureHelper surfaceTextureHelper, Context applicationContext, CapturerObserver capturerObserver)172 public void initialize(SurfaceTextureHelper surfaceTextureHelper, Context applicationContext, 173 CapturerObserver capturerObserver) { 174 this.capturerObserver = capturerObserver; 175 } 176 177 @Override startCapture(int width, int height, int framerate)178 public void startCapture(int width, int height, int framerate) { 179 timer.schedule(tickTask, 0, 1000 / framerate); 180 } 181 182 @Override stopCapture()183 public void stopCapture() throws InterruptedException { 184 timer.cancel(); 185 } 186 187 @Override changeCaptureFormat(int width, int height, int framerate)188 public void changeCaptureFormat(int width, int height, int framerate) { 189 // Empty on purpose 190 } 191 192 @Override dispose()193 public void dispose() { 194 videoReader.close(); 195 } 196 197 @Override isScreencast()198 public boolean isScreencast() { 199 return false; 200 } 201 } 202