| /* | |
| * 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(); | |
| 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; | |
| } | |
| } |