Skip to content

Commit

Permalink
Support to streaming IVF(VP8) format
Browse files Browse the repository at this point in the history
  • Loading branch information
JonesChi committed Jan 31, 2016
1 parent 1c660c6 commit b23065e
Show file tree
Hide file tree
Showing 6 changed files with 277 additions and 9 deletions.
46 changes: 37 additions & 9 deletions app/src/main/java/com/yschi/castscreen/CastService.java
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ public class CastService extends Service {
private String mReceiverIp;
private int mResultCode;
private Intent mResultData;
private String mSelectedFormat;
private int mSelectedWidth;
private int mSelectedHeight;
private int mSelectedDpi;
Expand All @@ -83,6 +84,7 @@ public class CastService extends Service {
private Socket mSocket;
private OutputStream mSocketOutputStream;
private BufferedOutputStream mFileOutputStream;
private IvfWriter mIvfWriter;
private Handler mDrainHandler = new Handler();
private Runnable mDrainEncoderRunnable = new Runnable() {
@Override
Expand Down Expand Up @@ -164,6 +166,10 @@ public int onStartCommand(Intent intent, int flags, int startId) {
mSelectedHeight = intent.getIntExtra(Common.EXTRA_SCREEN_HEIGHT, Common.DEFAULT_SCREEN_HEIGHT);
mSelectedDpi = intent.getIntExtra(Common.EXTRA_SCREEN_DPI, Common.DEFAULT_SCREEN_DPI);
mSelectedBitrate = intent.getIntExtra(Common.EXTRA_VIDEO_BITRATE, Common.DEFAULT_VIDEO_BITRATE);
mSelectedFormat = intent.getStringExtra(Common.EXTRA_VIDEO_FORMAT);
if (mSelectedFormat == null) {
mSelectedFormat = Common.DEFAULT_VIDEO_MIME_TYPE;
}
if (!createSocket()) {
Log.e(TAG, "Failed to create socket to receiver, ip: " + mReceiverIp);
return START_NOT_STICKY;
Expand Down Expand Up @@ -235,7 +241,7 @@ private void startRecording() {

private void prepareVideoEncoder() {
mVideoBufferInfo = new MediaCodec.BufferInfo();
MediaFormat format = MediaFormat.createVideoFormat(Common.DEFAULT_VIDEO_MIME_TYPE, mSelectedWidth, mSelectedHeight);
MediaFormat format = MediaFormat.createVideoFormat(mSelectedFormat, mSelectedWidth, mSelectedHeight);
int frameRate = Common.DEFAULT_VIDEO_FPS;

// Set some required properties. The media codec may fail if these aren't defined.
Expand All @@ -249,7 +255,7 @@ private void prepareVideoEncoder() {

// Create a MediaCodec encoder and configure it. Get a Surface we can use for recording into.
try {
mVideoEncoder = MediaCodec.createEncoderByType(Common.DEFAULT_VIDEO_MIME_TYPE);
mVideoEncoder = MediaCodec.createEncoderByType(mSelectedFormat);
mVideoEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
mInputSurface = mVideoEncoder.createInputSurface();
mVideoEncoder.start();
Expand Down Expand Up @@ -297,9 +303,16 @@ private boolean drainEncoder() {
try {
byte[] b = new byte[encodedData.remaining()];
encodedData.get(b);
mSocketOutputStream.write(b);
if (mIvfWriter != null) {
mIvfWriter.writeFrame(b, mVideoBufferInfo.presentationTimeUs);
} else {
mSocketOutputStream.write(b);
}
} catch (IOException e) {
Log.d(TAG, "Failed to write data to socket, stop casting");
e.printStackTrace();
stopScreenCapture();
return false;
}
}
/*
Expand Down Expand Up @@ -331,7 +344,7 @@ private boolean drainEncoder() {
}

mDrainHandler.postDelayed(mDrainEncoderRunnable, 10);
return false;
return true;
}

private void stopScreenCapture() {
Expand Down Expand Up @@ -370,6 +383,9 @@ private void releaseEncoders() {
mMediaProjection.stop();
mMediaProjection = null;
}
if (mIvfWriter != null) {
mIvfWriter = null;
}
mResultCode = 0;
mResultData = null;
mVideoBufferInfo = null;
Expand All @@ -387,12 +403,24 @@ public void run() {
OutputStreamWriter osw = new OutputStreamWriter(mSocketOutputStream);
osw.write(String.format(HTTP_MESSAGE_TEMPLATE, mSelectedWidth, mSelectedHeight));
osw.flush();
if (mSelectedWidth == 1280 && mSelectedHeight == 720) {
mSocketOutputStream.write(H264_PREDEFINED_HEADER_1280x720);
} else if (mSelectedWidth == 800 && mSelectedHeight == 480) {
mSocketOutputStream.write(H264_PREDEFINED_HEADER_800x480);
mSocketOutputStream.flush();
if (mSelectedFormat.equals(MediaFormat.MIMETYPE_VIDEO_AVC)) {
if (mSelectedWidth == 1280 && mSelectedHeight == 720) {
mSocketOutputStream.write(H264_PREDEFINED_HEADER_1280x720);
} else if (mSelectedWidth == 800 && mSelectedHeight == 480) {
mSocketOutputStream.write(H264_PREDEFINED_HEADER_800x480);
} else {
Log.e(TAG, "Unknown width: " + mSelectedWidth + ", height: " + mSelectedHeight);
mSocketOutputStream.close();
mSocket.close();
mSocket = null;
mSocketOutputStream = null;
}
} else if (mSelectedFormat.equals(MediaFormat.MIMETYPE_VIDEO_VP8)) {
mIvfWriter = new IvfWriter(mSocketOutputStream, mSelectedWidth, mSelectedHeight);
mIvfWriter.writeHeader();
} else {
Log.e(TAG, "Unknown width: " + mSelectedWidth + ", height: " + mSelectedHeight);
Log.e(TAG, "Unknown format: " + mSelectedFormat);
mSocketOutputStream.close();
mSocket.close();
mSocket = null;
Expand Down
1 change: 1 addition & 0 deletions app/src/main/java/com/yschi/castscreen/Common.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public class Common {
public static final String EXTRA_SCREEN_WIDTH = "screen_width";
public static final String EXTRA_SCREEN_HEIGHT = "screen_height";
public static final String EXTRA_SCREEN_DPI = "screen_dpi";
public static final String EXTRA_VIDEO_FORMAT = "video_format";
public static final String EXTRA_VIDEO_BITRATE = "video_bitrate";

public static final String ACTION_STOP_CAST = "com.yschi.castscreen.ACTION_STOP_CAST";
Expand Down
197 changes: 197 additions & 0 deletions app/src/main/java/com/yschi/castscreen/IvfWriter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.yschi.castscreen;

import java.io.IOException;
import java.io.OutputStream;
import java.io.RandomAccessFile;

/**
* Writes an IVF file.
*
* IVF format is a simple container format for VP8 encoded frames defined at
* http://wiki.multimedia.cx/index.php?title=IVF.
*/

public class IvfWriter {
private static final byte HEADER_END = 32;
//private RandomAccessFile mOutputFile;
private OutputStream mOutputStream;
private int mWidth;
private int mHeight;
private int mScale;
private int mRate;
private int mFrameCount;

/**
* Initializes the IVF file writer.
*
* Timebase fraction is in format scale/rate, e.g. 1/1000
* Timestamp values supplied while writing frames should be in accordance
* with this timebase value.
*
* @param filename name of the IVF file
* @param width frame width
* @param height frame height
* @param scale timebase scale (or numerator of the timebase fraction)
* @param rate timebase rate (or denominator of the timebase fraction)
*/
public IvfWriter(OutputStream outputStream,
int width, int height,
int scale, int rate) throws IOException {
//mOutputFile = new RandomAccessFile(filename, "rw");
mOutputStream = outputStream;
mWidth = width;
mHeight = height;
mScale = scale;
mRate = rate;
mFrameCount = 0;
//mOutputFile.setLength(0);
//mOutputFile.seek(HEADER_END); // Skip the header for now, as framecount is unknown
}

/**
* Initializes the IVF file writer with a microsecond timebase.
*
* Microsecond timebase is default for OMX thus stagefright.
*
* @param filename name of the IVF file
* @param width frame width
* @param height frame height
*/
public IvfWriter(OutputStream outputStream, int width, int height) throws IOException {
this(outputStream, width, height, 1, 1000000);
}

/**
* Finalizes the IVF header and closes the file.
*/
public void close() throws IOException{
// Write header now
//mOutputFile.seek(0);
//mOutputFile.write(makeIvfHeader(mFrameCount, mWidth, mHeight, mScale, mRate));
//mOutputFile.close();
mOutputStream.close();
}


public void writeHeader() throws IOException {
mOutputStream.write(makeIvfHeader(mFrameCount, mWidth, mHeight, mScale, mRate));
}

/**
* Writes a single encoded VP8 frame with its frame header.
*
* @param frame actual contents of the encoded frame data
* @param timeStamp timestamp of the frame (in accordance to specified timebase)
*/
public void writeFrame(byte[] frame, long timeStamp) throws IOException {
mOutputStream.write(makeIvfFrameHeader(frame.length, timeStamp));
mOutputStream.write(frame);
mFrameCount++;
}

/**
* Makes a 32 byte file header for IVF format.
*
* Timebase fraction is in format scale/rate, e.g. 1/1000
*
* @param frameCount total number of frames file contains
* @param width frame width
* @param height frame height
* @param scale timebase scale (or numerator of the timebase fraction)
* @param rate timebase rate (or denominator of the timebase fraction)
*/
public static byte[] makeIvfHeader(int frameCount, int width, int height, int scale, int rate){
byte[] ivfHeader = new byte[32];
ivfHeader[0] = 'D';
ivfHeader[1] = 'K';
ivfHeader[2] = 'I';
ivfHeader[3] = 'F';
lay16Bits(ivfHeader, 4, 0); // version
lay16Bits(ivfHeader, 6, 32); // header size
ivfHeader[8] = 'V'; // fourcc
ivfHeader[9] = 'P';
ivfHeader[10] = '8';
ivfHeader[11] = '0';
lay16Bits(ivfHeader, 12, width);
lay16Bits(ivfHeader, 14, height);
lay32Bits(ivfHeader, 16, rate); // scale/rate
lay32Bits(ivfHeader, 20, scale);
lay32Bits(ivfHeader, 24, frameCount);
lay32Bits(ivfHeader, 28, 0); // unused
return ivfHeader;
}

/**
* Makes a 12 byte header for an encoded frame.
*
* @param size frame size
* @param timestamp presentation timestamp of the frame
*/
private static byte[] makeIvfFrameHeader(int size, long timestamp){
byte[] frameHeader = new byte[12];
lay32Bits(frameHeader, 0, size);
lay64bits(frameHeader, 4, timestamp);
return frameHeader;
}


/**
* Lays least significant 16 bits of an int into 2 items of a byte array.
*
* Note that ordering is little-endian.
*
* @param array the array to be modified
* @param index index of the array to start laying down
* @param value the integer to use least significant 16 bits
*/
private static void lay16Bits(byte[] array, int index, int value){
array[index] = (byte) (value);
array[index + 1] = (byte) (value >> 8);
}

/**
* Lays an int into 4 items of a byte array.
*
* Note that ordering is little-endian.
*
* @param array the array to be modified
* @param index index of the array to start laying down
* @param value the integer to use
*/
private static void lay32Bits(byte[] array, int index, int value){
for (int i = 0; i < 4; i++){
array[index + i] = (byte) (value >> (i * 8));
}
}

/**
* Lays a long int into 8 items of a byte array.
*
* Note that ordering is little-endian.
*
* @param array the array to be modified
* @param index index of the array to start laying down
* @param value the integer to use
*/
private static void lay64bits(byte[] array, int index, long value){
for (int i = 0; i < 8; i++){
array[index + i] = (byte) (value >> (i * 8));
}
}
}
32 changes: 32 additions & 0 deletions app/src/main/java/com/yschi/castscreen/MainActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.media.MediaFormat;
import android.media.projection.MediaProjectionManager;
import android.os.AsyncTask;
import android.os.Bundle;
Expand Down Expand Up @@ -42,10 +43,17 @@ public class MainActivity extends Activity {
private static final String TAG = "MainActivity";

private static final String PREF_COMMON = "common";
private static final String PREF_KEY_INPUT_RECEIVER = "input_receiver";
private static final String PREF_KEY_FORMAT = "format";
private static final String PREF_KEY_RECEIVER = "receiver";
private static final String PREF_KEY_RESOLUTION = "resolution";
private static final String PREF_KEY_BITRATE = "bitrate";

private static final String[] FORMAT_OPTIONS = {
MediaFormat.MIMETYPE_VIDEO_AVC,
MediaFormat.MIMETYPE_VIDEO_VP8
};

private static final int[][] RESOLUTION_OPTIONS = {
{1280, 720, 320},
{800, 480, 160}
Expand All @@ -71,6 +79,7 @@ public class MainActivity extends Activity {
private ListView mDiscoverListView;
private ArrayAdapter<String> mDiscoverAdapter;
private HashMap<String, String> mDiscoverdMap;
private String mSelectedFormat = FORMAT_OPTIONS[0];
private int mSelectedWidth = RESOLUTION_OPTIONS[0][0];
private int mSelectedHeight = RESOLUTION_OPTIONS[0][1];
private int mSelectedDpi = RESOLUTION_OPTIONS[0][2];
Expand Down Expand Up @@ -152,10 +161,32 @@ public void onClick(View view) {
mReceiverIp = ipEditText.getText().toString();
Log.d(TAG, "Using ip: " + mReceiverIp);
updateReceiverStatus();
mContext.getSharedPreferences(PREF_COMMON, 0).edit().putString(PREF_KEY_INPUT_RECEIVER, mReceiverIp).commit();
mContext.getSharedPreferences(PREF_COMMON, 0).edit().putString(PREF_KEY_RECEIVER, mReceiverIp).commit();
}
}
});
ipEditText.setText(mContext.getSharedPreferences(PREF_COMMON, 0).getString(PREF_KEY_INPUT_RECEIVER, ""));

Spinner formatSpinner = (Spinner) findViewById(R.id.format_spinner);
ArrayAdapter<CharSequence> formatAdapter = ArrayAdapter.createFromResource(this,
R.array.format_options, android.R.layout.simple_spinner_item);
formatAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
formatSpinner.setAdapter(formatAdapter);
formatSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {
mSelectedFormat = FORMAT_OPTIONS[i];
mContext.getSharedPreferences(PREF_COMMON, 0).edit().putInt(PREF_KEY_FORMAT, i).commit();
}

@Override
public void onNothingSelected(AdapterView<?> adapterView) {
mSelectedFormat = FORMAT_OPTIONS[0];
mContext.getSharedPreferences(PREF_COMMON, 0).edit().putInt(PREF_KEY_FORMAT, 0).commit();
}
});
formatSpinner.setSelection(mContext.getSharedPreferences(PREF_COMMON, 0).getInt(PREF_KEY_FORMAT, 0));

Spinner resolutionSpinner = (Spinner) findViewById(R.id.resolution_spinner);
ArrayAdapter<CharSequence> resolutionAdapter = ArrayAdapter.createFromResource(this,
Expand Down Expand Up @@ -333,6 +364,7 @@ private void startService() {
intent.putExtra(Common.EXTRA_RESULT_CODE, mResultCode);
intent.putExtra(Common.EXTRA_RESULT_DATA, mResultData);
intent.putExtra(Common.EXTRA_RECEIVER_IP, mReceiverIp);
intent.putExtra(Common.EXTRA_VIDEO_FORMAT, mSelectedFormat);
intent.putExtra(Common.EXTRA_SCREEN_WIDTH, mSelectedWidth);
intent.putExtra(Common.EXTRA_SCREEN_HEIGHT, mSelectedHeight);
intent.putExtra(Common.EXTRA_SCREEN_DPI, mSelectedDpi);
Expand Down
Loading

0 comments on commit b23065e

Please sign in to comment.