|  | /* | 
|  | *  Copyright 2016 The WebRTC Project Authors. All rights reserved. | 
|  | * | 
|  | *  Use of this source code is governed by a BSD-style license | 
|  | *  that can be found in the LICENSE file in the root of the source | 
|  | *  tree. An additional intellectual property rights grant can be found | 
|  | *  in the file PATENTS.  All contributing project authors may | 
|  | *  be found in the AUTHORS file in the root of the source tree. | 
|  | */ | 
|  |  | 
|  | package org.webrtc; | 
|  |  | 
|  | import android.content.Context; | 
|  | import android.os.SystemClock; | 
|  | import java.io.IOException; | 
|  | import java.io.RandomAccessFile; | 
|  | import java.nio.ByteBuffer; | 
|  | import java.nio.channels.FileChannel; | 
|  | import java.nio.charset.Charset; | 
|  | import java.util.Timer; | 
|  | import java.util.TimerTask; | 
|  | import java.util.concurrent.TimeUnit; | 
|  |  | 
|  | public class FileVideoCapturer implements VideoCapturer { | 
|  | private interface VideoReader { | 
|  | VideoFrame getNextFrame(); | 
|  | void close(); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Read video data from file for the .y4m container. | 
|  | */ | 
|  | @SuppressWarnings("StringSplitter") | 
|  | private static class VideoReaderY4M implements VideoReader { | 
|  | private static final String TAG = "VideoReaderY4M"; | 
|  | private static final String Y4M_FRAME_DELIMETER = "FRAME"; | 
|  | private static final int FRAME_DELIMETER_LENGTH = Y4M_FRAME_DELIMETER.length() + 1; | 
|  |  | 
|  | private final int frameWidth; | 
|  | private final int frameHeight; | 
|  | // First char after header | 
|  | private final long videoStart; | 
|  | private final RandomAccessFile mediaFile; | 
|  | private final FileChannel mediaFileChannel; | 
|  |  | 
|  | public VideoReaderY4M(String file) throws IOException { | 
|  | mediaFile = new RandomAccessFile(file, "r"); | 
|  | mediaFileChannel = mediaFile.getChannel(); | 
|  | StringBuilder builder = new StringBuilder(); | 
|  | for (;;) { | 
|  | int c = mediaFile.read(); | 
|  | if (c == -1) { | 
|  | // End of file reached. | 
|  | throw new RuntimeException("Found end of file before end of header for file: " + file); | 
|  | } | 
|  | if (c == '\n') { | 
|  | // End of header found. | 
|  | break; | 
|  | } | 
|  | builder.append((char) c); | 
|  | } | 
|  | videoStart = mediaFileChannel.position(); | 
|  | String header = builder.toString(); | 
|  | String[] headerTokens = header.split("[ ]"); | 
|  | int w = 0; | 
|  | int h = 0; | 
|  | String colorSpace = ""; | 
|  | for (String tok : headerTokens) { | 
|  | char c = tok.charAt(0); | 
|  | switch (c) { | 
|  | case 'W': | 
|  | w = Integer.parseInt(tok.substring(1)); | 
|  | break; | 
|  | case 'H': | 
|  | h = Integer.parseInt(tok.substring(1)); | 
|  | break; | 
|  | case 'C': | 
|  | colorSpace = tok.substring(1); | 
|  | break; | 
|  | } | 
|  | } | 
|  | Logging.d(TAG, "Color space: " + colorSpace); | 
|  | if (!colorSpace.equals("420") && !colorSpace.equals("420mpeg2")) { | 
|  | throw new IllegalArgumentException( | 
|  | "Does not support any other color space than I420 or I420mpeg2"); | 
|  | } | 
|  | if ((w % 2) == 1 || (h % 2) == 1) { | 
|  | throw new IllegalArgumentException("Does not support odd width or height"); | 
|  | } | 
|  | frameWidth = w; | 
|  | frameHeight = h; | 
|  | Logging.d(TAG, "frame dim: (" + w + ", " + h + ")"); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public VideoFrame getNextFrame() { | 
|  | final long captureTimeNs = TimeUnit.MILLISECONDS.toNanos(SystemClock.elapsedRealtime()); | 
|  | final JavaI420Buffer buffer = JavaI420Buffer.allocate(frameWidth, frameHeight); | 
|  | final ByteBuffer dataY = buffer.getDataY(); | 
|  | final ByteBuffer dataU = buffer.getDataU(); | 
|  | final ByteBuffer dataV = buffer.getDataV(); | 
|  | final int chromaHeight = (frameHeight + 1) / 2; | 
|  | final int sizeY = frameHeight * buffer.getStrideY(); | 
|  | final int sizeU = chromaHeight * buffer.getStrideU(); | 
|  | final int sizeV = chromaHeight * buffer.getStrideV(); | 
|  |  | 
|  | try { | 
|  | ByteBuffer frameDelim = ByteBuffer.allocate(FRAME_DELIMETER_LENGTH); | 
|  | if (mediaFileChannel.read(frameDelim) < FRAME_DELIMETER_LENGTH) { | 
|  | // We reach end of file, loop | 
|  | mediaFileChannel.position(videoStart); | 
|  | if (mediaFileChannel.read(frameDelim) < FRAME_DELIMETER_LENGTH) { | 
|  | throw new RuntimeException("Error looping video"); | 
|  | } | 
|  | } | 
|  | String frameDelimStr = new String(frameDelim.array(), Charset.forName("US-ASCII")); | 
|  | if (!frameDelimStr.equals(Y4M_FRAME_DELIMETER + "\n")) { | 
|  | throw new RuntimeException( | 
|  | "Frames should be delimited by FRAME plus newline, found delimter was: '" | 
|  | + frameDelimStr + "'"); | 
|  | } | 
|  |  | 
|  | mediaFileChannel.read(dataY); | 
|  | mediaFileChannel.read(dataU); | 
|  | mediaFileChannel.read(dataV); | 
|  | } catch (IOException e) { | 
|  | throw new RuntimeException(e); | 
|  | } | 
|  |  | 
|  | return new VideoFrame(buffer, 0 /* rotation */, captureTimeNs); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void close() { | 
|  | try { | 
|  | // Closing a file also closes the channel. | 
|  | mediaFile.close(); | 
|  | } catch (IOException e) { | 
|  | Logging.e(TAG, "Problem closing file", e); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | private final static String TAG = "FileVideoCapturer"; | 
|  | private final VideoReader videoReader; | 
|  | private CapturerObserver capturerObserver; | 
|  | private final Timer timer = new Timer(); | 
|  |  | 
|  | private final TimerTask tickTask = new TimerTask() { | 
|  | @Override | 
|  | public void run() { | 
|  | tick(); | 
|  | } | 
|  | }; | 
|  |  | 
|  | public FileVideoCapturer(String inputFile) throws IOException { | 
|  | try { | 
|  | videoReader = new VideoReaderY4M(inputFile); | 
|  | } catch (IOException e) { | 
|  | Logging.d(TAG, "Could not open video file: " + inputFile); | 
|  | throw e; | 
|  | } | 
|  | } | 
|  |  | 
|  | public void tick() { | 
|  | VideoFrame videoFrame = videoReader.getNextFrame(); | 
|  | capturerObserver.onFrameCaptured(videoFrame); | 
|  | videoFrame.release(); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void initialize(SurfaceTextureHelper surfaceTextureHelper, Context applicationContext, | 
|  | CapturerObserver capturerObserver) { | 
|  | this.capturerObserver = capturerObserver; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void startCapture(int width, int height, int framerate) { | 
|  | timer.schedule(tickTask, 0, 1000 / framerate); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void stopCapture() throws InterruptedException { | 
|  | timer.cancel(); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void changeCaptureFormat(int width, int height, int framerate) { | 
|  | // Empty on purpose | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void dispose() { | 
|  | videoReader.close(); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public boolean isScreencast() { | 
|  | return false; | 
|  | } | 
|  | } |