加入推流相关的依赖

MQTTtest
Matthew 10 months ago
parent 34c6f8c341
commit 56538fec1a

@ -26,7 +26,7 @@ android {
applicationId "com.xypower.mpapp"
minSdk COMPILE_MIN_SDK_VERSION as int
//noinspection ExpiredTargetSdkVersion
targetSdk 28
targetSdk TARGET_SDK_VERSION as int
versionCode AppVersionCode
versionName AppVersionName
@ -135,9 +135,10 @@ dependencies {
// implementation 'com.tencent:mmkv-static:1.3.0'
// implementation project(path: ':opencv')
implementation files('libs/devapi.aar')
debugImplementation files('libs/rtmp-client-debug.aar')
releaseImplementation files('libs/rtmp-client.aar')
implementation project(':gpuv')
// debugImplementation files('libs/rtmp-client-debug.aar')
// releaseImplementation files('libs/rtmp-client.aar')
api project(':gpuv')
api project(':stream')
implementation 'dev.mobile:dadb:1.2.7'

@ -72,6 +72,15 @@
<uses-permission android:name="android.hardware.usb.accessory" />
<queries>
<provider
android:name=".BridgeProvider"
android:authorities="com.xypower.mpapp.provider"
android:enabled="true"
android:exported="false"
android:grantUriPermissions="true" />
</queries>
<application
android:dataExtractionRules="@xml/data_extraction_rules"
android:extractNativeLibs="true"
@ -83,17 +92,17 @@
android:supportsRtl="true"
android:theme="@style/Theme.MicroPhoto"
tools:targetApi="28">
<activity
android:name=".StreamActivity"
android:exported="false"
android:screenOrientation="landscape" />
<provider
android:name=".BridgeProvider"
android:authorities="com.xypower.mpapp.provider"
android:enabled="true"
android:exported="true"
android:grantUriPermissions="true" ></provider>
<service
android:name=".BridgeService"
android:enabled="true"
android:exported="true" />
android:grantUriPermissions="true" />
<activity
android:name=".BridgeActivity"
@ -186,16 +195,6 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
<queries>
<provider
android:name=".BridgeProvider"
android:authorities="com.xypower.mpapp.provider"
android:enabled="true"
android:exported="false"
android:grantUriPermissions="true" ></provider>
</queries>
</manifest>

@ -1,235 +0,0 @@
package com.xypower.mpapp;
import android.app.Service;
import android.content.Intent;
import android.os.Handler;
import android.os.IBinder;
import android.text.TextUtils;
import android.util.Base64;
import android.util.Log;
import androidx.annotation.Nullable;
import com.xypower.common.FilesUtils;
import com.xypower.common.JSONUtils;
import com.xypower.common.MicroPhotoContext;
import com.xypower.mpapp.v2.Camera2VideoActivity;
import org.json.JSONObject;
import java.io.File;
public class BridgeService extends Service {
private final static String TAG = "MPLOG";
private final static String ACTION_IMP_PUBKEY = "imp_pubkey";
private final static String ACTION_GEN_KEYS = "gen_keys";
private final static String ACTION_CERT_REQ = "cert_req";
private final static String ACTION_BATTERY_VOLTAGE = "query_bv";
private final static String ACTION_RECORDING = "recording";
private final static String ACTION_TAKE_PHOTO = "take_photo";
private final static int REQUEST_CODE_RECORDING = Camera2VideoActivity.REQUEST_CODE_RECORDING;
private Handler mHandler = null;
private boolean m3V3TurnedOn = false;
private boolean mAutoClose = true;
private String mVideoFilePath = null;
public BridgeService() {
}
@Override
public void onCreate() {
super.onCreate();
mHandler = new Handler();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (intent == null) {
stopSelf();
return START_NOT_STICKY;
}
final String action = intent.getStringExtra("action");
if (!TextUtils.isEmpty(action)) {
if (TextUtils.equals(action, ACTION_IMP_PUBKEY)) {
String cert = intent.getStringExtra("cert");
String path = intent.getStringExtra("path");
int index = intent.getIntExtra("index", 1);
if (!TextUtils.isEmpty(cert)) {
// Import
// String cert = intent.getStringExtra("md5");
byte[] content = Base64.decode(cert, Base64.DEFAULT);
if (content != null) {
MicroPhotoService.importPublicKey(index, content);
}
} else if (TextUtils.isEmpty(path)) {
String md5 = intent.getStringExtra("md5");
File file = new File(path);
if (file.exists() && file.isFile()) {
MicroPhotoService.importPublicKeyFile(index, path, md5);
}
}
} else if (TextUtils.equals(action, ACTION_GEN_KEYS)) {
int index = intent.getIntExtra("index", 0);
boolean res = MicroPhotoService.genKeys(index);
String path = intent.getStringExtra("path");
if (!TextUtils.isEmpty(path)) {
FilesUtils.ensureParentDirectoryExisted(path);
FilesUtils.writeTextFile(path, res ? "1" : "0");
}
} else if (TextUtils.equals(action, ACTION_CERT_REQ)) {
int index = intent.getIntExtra("index", 0);
int type = intent.getIntExtra("type", 0);
String subject = intent.getStringExtra("subject");
String path = intent.getStringExtra("path");
MicroPhotoService.genCertRequest(index, type, subject, path);
} else if (TextUtils.equals(action, ACTION_BATTERY_VOLTAGE)) {
String path = intent.getStringExtra("path");
// #define CMD_GET_CHARGING_BUS_VOLTAGE_STATE 112
// #define CMD_GET_BAT_VOL_STATE 115
// #define CMD_GET_BAT_BUS_VOLTAGE_STATE 117
int bv = MicroPhotoService.getGpioInt(117);
int bcv = MicroPhotoService.getGpioInt(112);
if (!TextUtils.isEmpty(path)) {
FilesUtils.ensureParentDirectoryExisted(path);
FilesUtils.writeTextFile(path + ".tmp", Integer.toString(bv) + " " + Integer.toString(bcv));
File file = new File(path + ".tmp");
file.renameTo(new File(path));
}
} else if (TextUtils.equals(action, ACTION_TAKE_PHOTO)) {
String path = intent.getStringExtra("path");
int channel = intent.getIntExtra("channel", 1);
int preset = intent.getIntExtra("preset", 0xFF);
int width = intent.getIntExtra("width", 0);
int height = intent.getIntExtra("height", 0);
String leftTopOsd = intent.getStringExtra("leftTopOsd");
String rightTopOsd = intent.getStringExtra("rightTopOsd");
String rightBottomOsd = intent.getStringExtra("rightBottomOsd");
String leftBottomOsd = intent.getStringExtra("leftBottomOsd");
String appPath = MicroPhotoContext.buildMpAppDir(getApplicationContext());
File configFile = new File(appPath);
configFile = new File(configFile, "data/channels/" + Integer.toString(channel) + ".json");
File tmpConfigFile = new File(appPath);
tmpConfigFile = new File(tmpConfigFile, "tmp/" + Integer.toString(channel) + "-" + Long.toString(System.currentTimeMillis()) + ".json");
if (configFile.exists()) {
try {
FilesUtils.copyFile(configFile, tmpConfigFile);
} catch (Exception ex) {
ex.printStackTrace();
}
}
JSONObject configJson = JSONUtils.loadJson(tmpConfigFile.getAbsolutePath());
try {
if (configJson == null) {
configJson = new JSONObject();
}
if (width > 0) {
configJson.put("resolutionCX", width);
}
if (height > 0) {
configJson.put("resolutionCY", height);
}
JSONObject osdJson = configJson.getJSONObject("osd");
if (osdJson == null) {
osdJson = configJson.put("osd", new JSONObject());
}
osdJson.put("leftTop", TextUtils.isEmpty(leftTopOsd) ? "" : leftTopOsd);
osdJson.put("rightTop", TextUtils.isEmpty(rightTopOsd) ? "" : rightTopOsd);
osdJson.put("rightBottom", TextUtils.isEmpty(rightBottomOsd) ? "" : rightBottomOsd);
osdJson.put("leftBottom", TextUtils.isEmpty(leftBottomOsd) ? "" : leftBottomOsd);
JSONUtils.saveJson(tmpConfigFile.getAbsolutePath(), configJson);
} catch (Exception ex) {
ex.printStackTrace();
}
File file = new File(path);
if (file.exists()) {
file.delete();
} else {
FilesUtils.ensureParentDirectoryExisted(path);
}
MicroPhotoService.takePhoto(channel, preset, true, tmpConfigFile.getAbsolutePath(), path);
if (tmpConfigFile.exists()) {
tmpConfigFile.delete();
}
} else if (TextUtils.equals(action, ACTION_RECORDING)) {
String path = intent.getStringExtra("path");
int channel = intent.getIntExtra("channel", 1);
int cameraId = intent.getIntExtra("cameraId", -1);
int quality = intent.getIntExtra("quality", 0);
int width = intent.getIntExtra("width", 1280);
int height = intent.getIntExtra("height", 720);
int duration = intent.getIntExtra("duration", 15);
int orientation = intent.getIntExtra("orientation", 0);
long videoId = System.currentTimeMillis() / 1000;
String leftTopOsd = intent.getStringExtra("leftTopOsd");
String rightTopOsd = intent.getStringExtra("rightTopOsd");
String rightBottomOsd = intent.getStringExtra("rightBottomOsd");
String leftBottomOsd = intent.getStringExtra("leftBottomOsd");
if (cameraId == -1) {
cameraId = channel - 1;
}
Intent recordingIntent = MicroPhotoService.makeRecordingIntent(getApplicationContext(),
cameraId, videoId, duration, width, height, quality, orientation,
leftTopOsd, rightTopOsd, rightBottomOsd, leftBottomOsd);
mVideoFilePath = path;
mAutoClose = false;
recordingIntent.putExtra("ActivityResult", true);
// startActivityForResult(recordingIntent, REQUEST_CODE_RECORDING);
}
}
if (mAutoClose) {
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
Log.i(TAG, "BridgeActivity will finish automatically");
stopSelf();
}
}, 200);
}
return super.onStartCommand(intent, flags, startId);
}
/**
*
*/
@Override
public void onDestroy() {
super.onDestroy();
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
}

@ -0,0 +1,49 @@
package com.xypower.mpapp;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Intent;
import android.os.Bundle;
import io.antmedia.rtmp_client.RTMPMuxer;
public class StreamActivity extends AppCompatActivity {
private RTMPMuxer mRtmpMuxer;
private String mUrl;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_stream);
Intent intent = getIntent();
if (intent != null) {
mUrl = intent.getStringExtra("url");
}
openRtmpMuxer(mUrl);
}
protected void openRtmpMuxer(String url) {
if (mRtmpMuxer != null) {
if (mRtmpMuxer.isConnected()) {
mRtmpMuxer.close();
}
} else {
mRtmpMuxer = new RTMPMuxer();
}
int result = mRtmpMuxer.open(url,0, 0);
}
@Override
protected void onDestroy() {
super.onDestroy();
if (mRtmpMuxer != null) {
mRtmpMuxer.close();
mRtmpMuxer = null;
}
}
}

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".StreamActivity">
</androidx.constraintlayout.widget.ConstraintLayout>

@ -6,8 +6,8 @@ android {
compileSdk 33
defaultConfig {
minSdk 24
targetSdk 28
minSdk COMPILE_MIN_SDK_VERSION as int
targetSdk TARGET_SDK_VERSION as int
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"

@ -14,8 +14,8 @@ android {
defaultConfig {
applicationId "com.xypower.mpmaster"
minSdk 25
targetSdk 28
minSdk COMPILE_MIN_SDK_VERSION as int
targetSdk TARGET_SDK_VERSION as int
versionCode AppVersionCode
versionName AppVersionName

@ -21,3 +21,4 @@ include ':gpuv'
// project(':opencv').projectDir = new File(opencvsdk + '/sdk')
include ':common'
include ':stream'

1
stream/.gitignore vendored

@ -0,0 +1 @@
/build

@ -0,0 +1,50 @@
/*
*
* *
* * * Copyright (C) 2017 ChillingVan
* * *
* * * 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.
* *
*
*/
apply plugin: 'com.android.library'
group='com.github.ChillingVan'
android {
compileSdkVersion 33
defaultConfig {
minSdkVersion COMPILE_MIN_SDK_VERSION as int
targetSdkVersion TARGET_SDK_VERSION as int
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
api 'com.github.ChillingVan:android-openGL-canvas:v1.5.3.0'
// implementation 'net.butterflytv.utils:rtmp-client:3.1.0'
debugImplementation files('libs/rtmp-client-debug.aar')
releaseImplementation files('libs/rtmp-client.aar')
implementation 'androidx.annotation:annotation:1.5.0'
}

@ -0,0 +1,17 @@
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in D:\develop-win\android\dev-tools-win\android-studio-windows\sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

@ -0,0 +1,31 @@
<!--
~ /*
~ *
~ * * Copyright (C) 2017 ChillingVan
~ * *
~ * * 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.
~ *
~ */
-->
<manifest package="com.chillingvan.instantvideo"
xmlns:android="http://schemas.android.com/apk/res/android">
<application android:allowBackup="true"
android:label="@string/app_name"
android:supportsRtl="true"
>
</application>
</manifest>

@ -0,0 +1,48 @@
/*
*
* *
* * * Copyright (C) 2017 ChillingVan
* * *
* * * 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.xypower.stream.camera;
import android.graphics.SurfaceTexture;
import android.hardware.Camera;
/**
* Created by Chilling on 2017/5/29.
*/
public interface CameraInterface {
void setPreview(SurfaceTexture surfaceTexture);
void openCamera();
void switchCamera();
void switchCamera(int previewWidth, int previewHeight);
boolean isOpened();
void startPreview();
void stopPreview();
Camera getCamera();
void release();
}

@ -0,0 +1,101 @@
/*
*
* *
* * * Copyright (C) 2017 ChillingVan
* * *
* * * 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.xypower.stream.camera;
import android.hardware.Camera;
import android.util.Log;
import java.util.List;
/**
* Camera-related utility functions.
*/
public class CameraUtils {
private static final String TAG = "tag";
/**
* Attempts to find a preview size that matches the provided width and height (which
* specify the dimensions of the encoded video). If it fails to find a match it just
* uses the default preview size for video.
* <p>
* TODO: should do a best-fit match, e.g.
* https://github.com/commonsguy/cwac-camera/blob/master/camera/src/com/commonsware/cwac/camera/CameraUtils.java
*/
public static void choosePreviewSize(Camera.Parameters parms, int width, int height) {
// We should make sure that the requested MPEG size is less than the preferred
// size, and has the same aspect ratio.
Camera.Size ppsfv = parms.getPreferredPreviewSizeForVideo();
if (ppsfv != null) {
Log.d(TAG, "Camera preferred preview size for video is " +
ppsfv.width + "x" + ppsfv.height);
}
//for (Camera.Size size : parms.getSupportedPreviewSizes()) {
// Log.d(TAG, "supported: " + size.width + "x" + size.height);
//}
for (Camera.Size size : parms.getSupportedPreviewSizes()) {
if (size.width == width && size.height == height) {
parms.setPreviewSize(width, height);
return;
}
}
Log.w(TAG, "Unable to set preview size to " + width + "x" + height);
if (ppsfv != null) {
parms.setPreviewSize(ppsfv.width, ppsfv.height);
}
// else use whatever the default size is
}
/**
* Attempts to find a fixed preview frame rate that matches the desired frame rate.
* <p>
* It doesn't seem like there's a great deal of flexibility here.
* <p>
* TODO: follow the recipe from http://stackoverflow.com/questions/22639336/#22645327
*
* @return The expected frame rate, in thousands of frames per second.
*/
public static int chooseFixedPreviewFps(Camera.Parameters parms, int desiredThousandFps) {
List<int[]> supported = parms.getSupportedPreviewFpsRange();
for (int[] entry : supported) {
//Log.d(TAG, "entry: " + entry[0] + " - " + entry[1]);
if ((entry[0] == entry[1]) && (entry[0] == desiredThousandFps)) {
parms.setPreviewFpsRange(entry[0], entry[1]);
return entry[0];
}
}
int[] tmp = new int[2];
parms.getPreviewFpsRange(tmp);
int guess;
if (tmp[0] == tmp[1]) {
guess = tmp[0];
} else {
guess = tmp[1] / 2; // shrug
}
Log.d(TAG, "Couldn't find match for " + desiredThousandFps + ", using " + guess);
return guess;
}
}

@ -0,0 +1,123 @@
/*
*
* *
* * * Copyright (C) 2017 ChillingVan
* * *
* * * 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.xypower.stream.camera;
import android.graphics.SurfaceTexture;
import android.hardware.Camera;
import java.io.IOException;
/**
* Created by Chilling on 2016/12/10.
*/
public class InstantVideoCamera implements CameraInterface {
private Camera camera;
private boolean isOpened;
private int currentCamera;
private int previewWidth;
private int previewHeight;
@Override
public void setPreview(SurfaceTexture surfaceTexture) {
try {
camera.setPreviewTexture(surfaceTexture);
} catch (IOException e) {
e.printStackTrace();
}
}
public InstantVideoCamera(int currentCamera, int previewWidth, int previewHeight) {
this.currentCamera = currentCamera;
this.previewWidth = previewWidth;
this.previewHeight = previewHeight;
}
@Override
public void openCamera() {
Camera.CameraInfo info = new Camera.CameraInfo();
// Try to find a front-facing camera (e.g. for videoconferencing).
int numCameras = Camera.getNumberOfCameras();
for (int i = 0; i < numCameras; i++) {
Camera.getCameraInfo(i, info);
if (info.facing == currentCamera) {
camera = Camera.open(i);
break;
}
}
if (camera == null) {
camera = Camera.open();
}
Camera.Parameters parms = camera.getParameters();
CameraUtils.choosePreviewSize(parms, previewWidth, previewHeight);
isOpened = true;
}
@Override
public void switchCamera() {
switchCamera(previewWidth, previewHeight);
}
@Override
public void switchCamera(int previewWidth, int previewHeight) {
this.previewWidth = previewWidth;
this.previewHeight = previewHeight;
release();
currentCamera = currentCamera == Camera.CameraInfo.CAMERA_FACING_BACK ? Camera.CameraInfo.CAMERA_FACING_FRONT : Camera.CameraInfo.CAMERA_FACING_BACK;
openCamera();
}
@Override
public boolean isOpened() {
return isOpened;
}
@Override
public void startPreview() {
camera.startPreview();
}
@Override
public void stopPreview() {
camera.stopPreview();
}
@Override
public Camera getCamera() {
return camera;
}
@Override
public void release() {
if (isOpened) {
camera.stopPreview();
camera.release();
camera = null;
isOpened = false;
}
}
}

@ -0,0 +1,166 @@
/*
*
* *
* * * Copyright (C) 2017 ChillingVan
* * *
* * * 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.xypower.stream.encoder;
import android.annotation.SuppressLint;
import android.media.MediaCodec;
import android.media.MediaCodec.BufferInfo;
import android.media.MediaFormat;
import android.os.Build;
import androidx.annotation.NonNull;
import android.util.Log;
import com.chillingvan.canvasgl.util.Loggers;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
@SuppressLint("NewApi")
public class MediaCodecInputStream extends InputStream {
public final String TAG = "MediaCodecInputStream";
private MediaCodec mMediaCodec = null;
private MediaFormatCallback mediaFormatCallback;
private BufferInfo mBufferInfo = new BufferInfo();
private ByteBuffer mBuffer = null;
private boolean mClosed = false;
public MediaFormat mMediaFormat;
private ByteBuffer[] encoderOutputBuffers;
public MediaCodecInputStream(MediaCodec mediaCodec, MediaFormatCallback mediaFormatCallback) {
mMediaCodec = mediaCodec;
this.mediaFormatCallback = mediaFormatCallback;
}
@Override
public void close() {
mClosed = true;
}
@Deprecated
@Override
public int read() throws IOException {
return 0;
}
@Override
public int read(byte[] buffer, int offset, int length) throws IOException {
int readLength;
int encoderStatus = -1;
if (mBuffer == null) {
while (!Thread.interrupted() && !mClosed) {
synchronized (mMediaCodec) {
if (mClosed) return 0;
// timeout should not bigger than 0 for clear voice
encoderStatus = mMediaCodec.dequeueOutputBuffer(mBufferInfo, 0);
Loggers.d(TAG, "Index: " + encoderStatus + " Time: " + mBufferInfo.presentationTimeUs + " size: " + mBufferInfo.size);
if (encoderStatus >= 0) {
if (Build.VERSION.SDK_INT >= 21) {
mBuffer = mMediaCodec.getOutputBuffer(encoderStatus);
} else {
mBuffer = mMediaCodec.getOutputBuffers()[encoderStatus];
}
mBuffer.position(mBufferInfo.offset);
mBuffer.limit(mBufferInfo.offset + mBufferInfo.size);
break;
} else if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
mMediaFormat = mMediaCodec.getOutputFormat();
if (mediaFormatCallback != null) {
mediaFormatCallback.onChangeMediaFormat(mMediaFormat);
}
Log.i(TAG, mMediaFormat.toString());
} else if (encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {
Log.v(TAG, "No buffer available...");
return 0;
} else {
Log.e(TAG, "Message: " + encoderStatus);
return 0;
}
if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
return 0;
}
}
}
}
if (mClosed) throw new IOException("This InputStream was closed");
readLength = length < mBufferInfo.size - mBuffer.position() ? length :
mBufferInfo.size - mBuffer.position();
mBuffer.get(buffer, offset, readLength);
if (mBuffer.position() >= mBufferInfo.size) {
mMediaCodec.releaseOutputBuffer(encoderStatus, false);
mBuffer = null;
}
return readLength;
}
public int available() {
if (mBuffer != null)
return mBufferInfo.size - mBuffer.position();
else
return 0;
}
public BufferInfo getLastBufferInfo() {
return mBufferInfo;
}
public static void readAll(MediaCodecInputStream is, byte[] buffer, @NonNull OnReadAllCallback onReadAllCallback) {
byte[] readBuf = buffer;
int readSize = 0;
do {
try {
readSize = is.read(readBuf, 0, readBuf.length);
onReadAllCallback.onReadOnce(readBuf, readSize, copyBufferInfo(is.getLastBufferInfo()));
} catch (IOException e) {
e.printStackTrace();
return;
}
} while (readSize > 0);
}
@NonNull
private static BufferInfo copyBufferInfo(BufferInfo lastBufferInfo) {
BufferInfo bufferInfo = new BufferInfo();
bufferInfo.presentationTimeUs = lastBufferInfo.presentationTimeUs;
bufferInfo.flags = lastBufferInfo.flags;
bufferInfo.offset = lastBufferInfo.offset;
bufferInfo.size = lastBufferInfo.size;
return bufferInfo;
}
public interface OnReadAllCallback {
void onReadOnce(byte[] buffer, int readSize, BufferInfo mediaBufferSize);
}
public interface MediaFormatCallback {
void onChangeMediaFormat(MediaFormat mediaFormat);
}
}

@ -0,0 +1,176 @@
/*
*
* *
* * * Copyright (C) 2017 ChillingVan
* * *
* * * 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.xypower.stream.encoder.audio;
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaFormat;
import android.media.audiofx.AcousticEchoCanceler;
import android.media.audiofx.AutomaticGainControl;
import android.media.audiofx.NoiseSuppressor;
import android.util.Log;
import com.chillingvan.canvasgl.util.Loggers;
import com.xypower.stream.encoder.MediaCodecInputStream;
import com.xypower.stream.publisher.StreamPublisher;
import java.io.IOException;
import java.nio.ByteBuffer;
/**
* Data Stream:
* MIC -> AudioRecord -> voice data(byte[]) -> MediaCodec -> encode data(byte[])
*/
public class AACEncoder {
private static final String TAG = "AACEncoder";
private AudioRecord mAudioRecord;
private final MediaCodec mMediaCodec;
private final MediaCodecInputStream mediaCodecInputStream;
private Thread mThread;
private OnDataComingCallback onDataComingCallback;
private int samplingRate;
private final int bufferSize;
private boolean isStart;
public AACEncoder(final StreamPublisher.StreamPublisherParam params) throws IOException {
this.samplingRate = params.samplingRate;
bufferSize = params.audioBufferSize;
mMediaCodec = MediaCodec.createEncoderByType(params.audioMIME);
mMediaCodec.configure(params.createAudioMediaFormat(), null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
mediaCodecInputStream = new MediaCodecInputStream(mMediaCodec, new MediaCodecInputStream.MediaFormatCallback() {
@Override
public void onChangeMediaFormat(MediaFormat mediaFormat) {
params.setAudioOutputMediaFormat(mediaFormat);
}
});
mAudioRecord = new AudioRecord(params.audioSource, samplingRate, params.channelCfg, AudioFormat.ENCODING_PCM_16BIT, bufferSize * 10);
if (NoiseSuppressor.isAvailable()) {
NoiseSuppressor noiseSuppressor = NoiseSuppressor.create(mAudioRecord.getAudioSessionId());
if (noiseSuppressor != null) {
noiseSuppressor.setEnabled(true);
}
}
if (AcousticEchoCanceler.isAvailable()) {
AcousticEchoCanceler aec = AcousticEchoCanceler.create(mAudioRecord.getAudioSessionId());
if (aec != null) {
aec.setEnabled(true); //android 11 issue low volume
}
}
if (AutomaticGainControl.isAvailable()) {
AutomaticGainControl agc = AutomaticGainControl.create(mAudioRecord.getAudioSessionId());
if (agc != null) {
agc.setEnabled(true);
}
}
}
public void start() {
mAudioRecord.startRecording();
mMediaCodec.start();
final long startWhen = System.nanoTime();
final ByteBuffer[] inputBuffers = mMediaCodec.getInputBuffers();
mThread = new Thread(new Runnable() {
@Override
public void run() {
int len, bufferIndex;
while (isStart && !Thread.interrupted()) {
synchronized (mMediaCodec) {
if (!isStart) return;
bufferIndex = mMediaCodec.dequeueInputBuffer(10000);
if (bufferIndex >= 0) {
inputBuffers[bufferIndex].clear();
long presentationTimeNs = System.nanoTime();
len = mAudioRecord.read(inputBuffers[bufferIndex], bufferSize);
presentationTimeNs -= (len / samplingRate ) / 1000000000;
Loggers.i(TAG, "Index: " + bufferIndex + " len: " + len + " buffer_capacity: " + inputBuffers[bufferIndex].capacity());
long presentationTimeUs = (presentationTimeNs - startWhen) / 1000;
if (len == AudioRecord.ERROR_INVALID_OPERATION || len == AudioRecord.ERROR_BAD_VALUE) {
Log.e(TAG, "An error occured with the AudioRecord API !");
} else {
mMediaCodec.queueInputBuffer(bufferIndex, 0, len, presentationTimeUs, 0);
if (onDataComingCallback != null) {
onDataComingCallback.onComing();
}
}
}
}
}
}
});
mThread.start();
isStart = true;
}
public static void addADTStoPacket(byte[] packet, int packetLen, int channelCnt) {
int profile = MediaCodecInfo.CodecProfileLevel.AACObjectLC; // AAC LC
int freqIdx = 4; // 44.1KHz
int chanCfg = channelCnt; // CPE channel cnt
// fill in ADTS data
packet[0] = (byte) 0xFF;
packet[1] = (byte) 0xF9;
packet[2] = (byte) (((profile - 1) << 6) + (freqIdx << 2) + (chanCfg >> 2));
packet[3] = (byte) (((chanCfg & 3) << 6) + (packetLen >> 11));
packet[4] = (byte) ((packetLen & 0x7FF) >> 3);
packet[5] = (byte) (((packetLen & 7) << 5) + 0x1F);
packet[6] = (byte) 0xFC;
}
public void setOnDataComingCallback(OnDataComingCallback onDataComingCallback) {
this.onDataComingCallback = onDataComingCallback;
}
public interface OnDataComingCallback {
void onComing();
}
public MediaCodecInputStream getMediaCodecInputStream() {
return mediaCodecInputStream;
}
public synchronized void close() {
if (!isStart) {
return;
}
Loggers.d(TAG, "Interrupting threads...");
isStart = false;
mThread.interrupt();
mediaCodecInputStream.close();
synchronized (mMediaCodec) {
mMediaCodec.stop();
mMediaCodec.release();
}
mAudioRecord.stop();
mAudioRecord.release();
}
}

@ -0,0 +1,186 @@
/*
*
* *
* * * Copyright (C) 2017 ChillingVan
* * *
* * * 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.xypower.stream.encoder.video;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaFormat;
import android.view.Surface;
import com.chillingvan.canvasgl.ICanvasGL;
import com.chillingvan.canvasgl.MultiTexOffScreenCanvas;
import com.chillingvan.canvasgl.glview.texture.GLTexture;
import com.chillingvan.canvasgl.glview.texture.gles.EglContextWrapper;
import com.chillingvan.canvasgl.util.Loggers;
import com.xypower.stream.encoder.MediaCodecInputStream;
import com.xypower.stream.publisher.StreamPublisher;
import java.io.IOException;
import java.util.List;
/**
* Data Stream:
*
* The texture of {@link H264Encoder#addSharedTexture} -> Surface of MediaCodec -> encode data(byte[])
*
*/
public class H264Encoder {
private final Surface mInputSurface;
private final MediaCodecInputStream mediaCodecInputStream;
MediaCodec mEncoder;
private static final String MIME_TYPE = "video/avc"; // H.264 Advanced Video Coding
private static final int FRAME_RATE = 30; // 30fps
private static final int IFRAME_INTERVAL = 5; // 5 seconds between I-frames
protected final EncoderCanvas offScreenCanvas;
private OnDrawListener onDrawListener;
private boolean isStart;
private int initialTextureCount = 1;
public H264Encoder(StreamPublisher.StreamPublisherParam params) throws IOException {
this(params, EglContextWrapper.EGL_NO_CONTEXT_WRAPPER);
}
/**
*
* @param eglCtx can be EGL10.EGL_NO_CONTEXT or outside context
*/
public H264Encoder(final StreamPublisher.StreamPublisherParam params, final EglContextWrapper eglCtx) throws IOException {
// MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, params.width, params.height);
MediaFormat format = params.createVideoMediaFormat();
// Set some properties. Failing to specify some of these can cause the MediaCodec
// configure() call to throw an unhelpful exception.
format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
format.setInteger(MediaFormat.KEY_BIT_RATE, params.videoBitRate);
format.setInteger(MediaFormat.KEY_FRAME_RATE, params.frameRate);
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, params.iframeInterval);
mEncoder = MediaCodec.createEncoderByType(params.videoMIMEType);
mEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
mInputSurface = mEncoder.createInputSurface();
mEncoder.start();
mediaCodecInputStream = new MediaCodecInputStream(mEncoder, new MediaCodecInputStream.MediaFormatCallback() {
@Override
public void onChangeMediaFormat(MediaFormat mediaFormat) {
params.setVideoOutputMediaFormat(mediaFormat);
}
});
this.initialTextureCount = params.getInitialTextureCount();
offScreenCanvas = new EncoderCanvas(params.width, params.height, eglCtx);
}
/**
* If called, should be called before start() called.
*/
public void addSharedTexture(GLTexture texture) {
offScreenCanvas.addConsumeGLTexture(texture);
}
public Surface getInputSurface() {
return mInputSurface;
}
public MediaCodecInputStream getMediaCodecInputStream() {
return mediaCodecInputStream;
}
/**
*
* @param initialTextureCount Default is 1
*/
public void setInitialTextureCount(int initialTextureCount) {
if (initialTextureCount < 1) {
throw new IllegalArgumentException("initialTextureCount must >= 1");
}
this.initialTextureCount = initialTextureCount;
}
public void start() {
offScreenCanvas.start();
isStart = true;
}
public void close() {
if (!isStart) return;
Loggers.d("H264Encoder", "close");
offScreenCanvas.end();
mediaCodecInputStream.close();
synchronized (mEncoder) {
mEncoder.stop();
mEncoder.release();
}
isStart = false;
}
public boolean isStart() {
return isStart;
}
public void requestRender() {
offScreenCanvas.requestRender();
}
public void requestRenderAndWait() {
offScreenCanvas.requestRenderAndWait();
}
public void setOnDrawListener(OnDrawListener l) {
this.onDrawListener = l;
}
public interface OnDrawListener {
/**
* Called when a frame is ready to be drawn.
* @param canvasGL The gl canvas
* @param producedTextures The textures produced by internal. These can be used for camera or video decoder to render.
* @param consumedTextures See {@link #addSharedTexture(GLTexture)}. The textures you set from outside. Then you can draw the textures render by other Views of OffscreenCanvas.
*/
void onGLDraw(ICanvasGL canvasGL, List<GLTexture> producedTextures, List<GLTexture> consumedTextures);
}
private class EncoderCanvas extends MultiTexOffScreenCanvas {
public EncoderCanvas(int width, int height, EglContextWrapper eglCtx) {
super(width, height, eglCtx, H264Encoder.this.mInputSurface);
}
@Override
protected void onGLDraw(ICanvasGL canvas, List<GLTexture> producedTextures, List<GLTexture> consumedTextures) {
if (onDrawListener != null) {
onDrawListener.onGLDraw(canvas, producedTextures, consumedTextures);
}
}
@Override
protected int getInitialTexCount() {
return initialTextureCount;
}
}
}

@ -0,0 +1,52 @@
/*
*
* *
* * * Copyright (C) 2017 ChillingVan
* * *
* * * 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.xypower.stream.muxer;
import android.media.MediaCodec;
import com.xypower.stream.publisher.StreamPublisher;
/**
* Created by Chilling on 2017/12/23.
*/
public abstract class BaseMuxer implements IMuxer {
protected TimeIndexCounter videoTimeIndexCounter = new TimeIndexCounter();
protected TimeIndexCounter audioTimeIndexCounter = new TimeIndexCounter();
@Override
public int open(StreamPublisher.StreamPublisherParam params) {
videoTimeIndexCounter.reset();
audioTimeIndexCounter.reset();
return 0;
}
@Override
public void writeVideo(byte[] buffer, int offset, int length, MediaCodec.BufferInfo bufferInfo) {
videoTimeIndexCounter.calcTotalTime(bufferInfo.presentationTimeUs);
}
@Override
public void writeAudio(byte[] buffer, int offset, int length, MediaCodec.BufferInfo bufferInfo) {
audioTimeIndexCounter.calcTotalTime(bufferInfo.presentationTimeUs);
}
}

@ -0,0 +1,45 @@
/*
*
* *
* * * Copyright (C) 2017 ChillingVan
* * *
* * * 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.xypower.stream.muxer;
import android.media.MediaCodec;
/**
* Created by Chilling on 2017/12/23.
*/
public class BufferInfoEx {
private MediaCodec.BufferInfo bufferInfo;
private int totalTime;
public BufferInfoEx(MediaCodec.BufferInfo bufferInfo, int totalTime) {
this.bufferInfo = bufferInfo;
this.totalTime = totalTime;
}
public MediaCodec.BufferInfo getBufferInfo() {
return bufferInfo;
}
public int getTotalTime() {
return totalTime;
}
}

@ -0,0 +1,155 @@
/*
*
* *
* * * Copyright (C) 2017 ChillingVan
* * *
* * * 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.xypower.stream.muxer;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/**
* Created by Chilling on 2017/6/10.
*/
public class FramePool {
private final ItemsPool<Frame> frameItemsPool;
public FramePool(int poolSize) {
frameItemsPool = new ItemsPool<>(poolSize);
}
public Frame obtain(byte[] data, int offset, int length, BufferInfoEx bufferInfo, int type) {
Frame frame = frameItemsPool.acquire();
if (frame == null) {
frame = new Frame(data, offset, length, bufferInfo, type);
} else {
frame.set(data, offset, length, bufferInfo, type);
}
return frame;
}
public void release(Frame frame) {
frameItemsPool.release(frame);
}
private static class ItemsPool<T> {
private final Object[] mPool;
private int mPoolSize;
/**
* Creates a new instance.
*
* @param maxPoolSize The max pool size.
* @throws IllegalArgumentException If the max pool size is less than zero.
*/
public ItemsPool(int maxPoolSize) {
if (maxPoolSize <= 0) {
throw new IllegalArgumentException("The max pool size must be > 0");
}
mPool = new Object[maxPoolSize];
}
@SuppressWarnings("unchecked")
public T acquire() {
if (mPoolSize > 0) {
final int lastPooledIndex = mPoolSize - 1;
T instance = (T) mPool[lastPooledIndex];
mPool[lastPooledIndex] = null;
mPoolSize--;
return instance;
}
return null;
}
public boolean release(T instance) {
if (isInPool(instance)) {
return false;
}
if (mPoolSize < mPool.length) {
mPool[mPoolSize] = instance;
mPoolSize++;
return true;
}
return false;
}
private boolean isInPool(T instance) {
for (int i = 0; i < mPoolSize; i++) {
if (mPool[i] == instance) {
return true;
}
}
return false;
}
}
public static class Frame {
public byte[] data;
public int length;
public BufferInfoEx bufferInfo;
public int type;
public static final int TYPE_VIDEO = 1;
public static final int TYPE_AUDIO = 2;
public Frame(byte[] data, int offset, int length, BufferInfoEx bufferInfo, int type) {
this.data = new byte[length];
init(data, offset, length, bufferInfo, type);
}
public void set(byte[] data, int offset, int length, BufferInfoEx bufferInfo, int type) {
if (this.data.length < length) {
this.data = new byte[length];
}
init(data, offset, length, bufferInfo, type);
}
private void init(byte[] data, int offset, int length, BufferInfoEx bufferInfo, int type) {
System.arraycopy(data, offset, this.data, 0, length);
this.length = length;
this.bufferInfo = bufferInfo;
this.type = type;
bufferInfo.getBufferInfo().size = length;
bufferInfo.getBufferInfo().offset = 0;
bufferInfo.getBufferInfo().presentationTimeUs = bufferInfo.getTotalTime() * 1000;
}
public static void sortFrame(List<Frame> frameQueue) {
Collections.sort(frameQueue, new Comparator<Frame>() {
@Override
public int compare(Frame left, Frame right) {
if (left.bufferInfo.getTotalTime() < right.bufferInfo.getTotalTime()) {
return -1;
} else if (left.bufferInfo.getTotalTime() == right.bufferInfo.getTotalTime()) {
return 0;
} else {
return 1;
}
}
});
}
}
}

@ -0,0 +1,123 @@
/*
*
* *
* * * Copyright (C) 2017 ChillingVan
* * *
* * * 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.xypower.stream.muxer;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Message;
import java.util.LinkedList;
import java.util.List;
/**
* Created by Chilling on 2017/12/17.
*/
public class FrameSender {
private static final int KEEP_COUNT = 30;
private static final int MESSAGE_READY_TO_CLOSE = 4;
private static final int MSG_ADD_FRAME = 3;
private static final int MSG_START = 2;
private Handler sendHandler;
private List<FramePool.Frame> frameQueue = new LinkedList<>();
private FramePool framePool = new FramePool(KEEP_COUNT + 10);
private FrameSenderCallback frameSenderCallback;
public FrameSender(final FrameSenderCallback frameSenderCallback) {
this.frameSenderCallback = frameSenderCallback;
final HandlerThread sendHandlerThread = new HandlerThread("send_thread");
sendHandlerThread.start();
sendHandler = new Handler(sendHandlerThread.getLooper()) {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
if (msg.what == MESSAGE_READY_TO_CLOSE) {
if (msg.obj != null) {
addFrame((FramePool.Frame) msg.obj);
}
sendFrame(msg.arg1);
frameSenderCallback.close();
sendHandlerThread.quitSafely();
} else if (msg.what == MSG_ADD_FRAME) {
if (msg.obj != null) {
addFrame((FramePool.Frame) msg.obj);
}
sendFrame(msg.arg1);
} else if (msg.what == MSG_START) {
frameSenderCallback.onStart();
}
}
};
}
private void addFrame(FramePool.Frame frame) {
frameQueue.add(frame);
FramePool.Frame.sortFrame(frameQueue);
}
private void sendFrame(int keepCount) {
while (frameQueue.size() > keepCount) {
FramePool.Frame sendFrame = frameQueue.remove(0);
if (sendFrame.type == FramePool.Frame.TYPE_VIDEO) {
frameSenderCallback.onSendVideo(sendFrame);
} else if(sendFrame.type == FramePool.Frame.TYPE_AUDIO) {
frameSenderCallback.onSendAudio(sendFrame);
}
framePool.release(sendFrame);
}
}
public void sendStartMessage() {
Message message = Message.obtain();
message.what = MSG_START;
sendHandler.sendMessage(message);
}
public void sendAddFrameMessage(byte[] data, int offset, int length, BufferInfoEx bufferInfo, int type) {
FramePool.Frame frame = framePool.obtain(data, offset, length, bufferInfo, type);
Message message = Message.obtain();
message.what = MSG_ADD_FRAME;
message.obj = frame;
message.arg1 = KEEP_COUNT;
sendHandler.sendMessage(message);
}
public void sendCloseMessage() {
Message message = Message.obtain();
message.arg1 = 0;
message.what = MESSAGE_READY_TO_CLOSE;
sendHandler.sendMessage(message);
}
public interface FrameSenderCallback {
void onStart();
void onSendVideo(FramePool.Frame sendFrame);
void onSendAudio(FramePool.Frame sendFrame);
void close();
}
}

@ -0,0 +1,45 @@
/*
*
* *
* * * Copyright (C) 2017 ChillingVan
* * *
* * * 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.xypower.stream.muxer;
import android.media.MediaCodec;
import com.xypower.stream.publisher.StreamPublisher;
/**
* Created by Chilling on 2017/5/21.
*/
public interface IMuxer {
/**
*
* @return 1 if it is connected
* 0 if it is not connected
*/
int open(StreamPublisher.StreamPublisherParam params);
void writeVideo(byte[] buffer, int offset, int length, MediaCodec.BufferInfo bufferInfo);
void writeAudio(byte[] buffer, int offset, int length, MediaCodec.BufferInfo bufferInfo);
int close();
}

@ -0,0 +1,155 @@
/*
*
* *
* * * Copyright (C) 2017 ChillingVan
* * *
* * * 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.xypower.stream.muxer;
import android.media.MediaCodec;
import android.media.MediaFormat;
import android.media.MediaMuxer;
import com.chillingvan.canvasgl.util.Loggers;
import com.xypower.stream.publisher.StreamPublisher;
import java.io.IOException;
import java.nio.ByteBuffer;
/**
* Created by Chilling on 2017/12/17.
*/
public class MP4Muxer extends BaseMuxer {
private static final String TAG = "MP4Muxer";
private MediaMuxer mMuxer;
private Integer videoTrackIndex = null;
private Integer audioTrackIndex = null;
private int trackCnt = 0;
private FrameSender frameSender;
private StreamPublisher.StreamPublisherParam params;
private boolean isStart;
public MP4Muxer() {
super();
}
@Override
public int open(final StreamPublisher.StreamPublisherParam params) {
isStart = false;
trackCnt = 0;
videoTrackIndex = null;
audioTrackIndex = null;
this.params = params;
super.open(params);
try {
mMuxer = new MediaMuxer(params.outputFilePath,
MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
} catch (IOException e) {
e.printStackTrace();
}
frameSender = new FrameSender(new FrameSender.FrameSenderCallback(){
@Override
public void onStart() {
isStart = true;
mMuxer.start();
}
@Override
public void onSendVideo(FramePool.Frame sendFrame) {
if (isStart) {
mMuxer.writeSampleData(videoTrackIndex, ByteBuffer.wrap(sendFrame.data), sendFrame.bufferInfo.getBufferInfo());
}
}
@Override
public void onSendAudio(FramePool.Frame sendFrame) {
if (isStart) {
mMuxer.writeSampleData(audioTrackIndex, ByteBuffer.wrap(sendFrame.data), sendFrame.bufferInfo.getBufferInfo());
}
}
@Override
public void close() {
isStart = false;
if (mMuxer != null) {
mMuxer.stop();
mMuxer.release();
mMuxer = null;
}
}
});
return 1;
}
@Override
public void writeVideo(byte[] buffer, int offset, int length, MediaCodec.BufferInfo bufferInfo) {
super.writeVideo(buffer, offset, length, bufferInfo);
addTrackAndReadyToStart(FramePool.Frame.TYPE_VIDEO);
Loggers.d(TAG, "writeVideo: " + " offset:" + offset + " length:" + length);
frameSender.sendAddFrameMessage(buffer, offset, length, new BufferInfoEx(bufferInfo, videoTimeIndexCounter.getTimeIndex()), FramePool.Frame.TYPE_VIDEO);
}
@Override
public void writeAudio(byte[] buffer, int offset, int length, MediaCodec.BufferInfo bufferInfo) {
super.writeAudio(buffer, offset, length, bufferInfo);
addTrackAndReadyToStart(FramePool.Frame.TYPE_AUDIO);
Loggers.d(TAG, "writeAudio: ");
frameSender.sendAddFrameMessage(buffer, offset, length, new BufferInfoEx(bufferInfo, audioTimeIndexCounter.getTimeIndex()), FramePool.Frame.TYPE_AUDIO);
}
private void addTrackAndReadyToStart(int type) {
if (trackCnt == 2) {
return;
}
if (videoTrackIndex == null && type == FramePool.Frame.TYPE_VIDEO) {
MediaFormat videoOutputMediaFormat = params.getVideoOutputMediaFormat();
videoOutputMediaFormat.getByteBuffer("csd-0"); // SPS
videoOutputMediaFormat.getByteBuffer("csd-1"); // PPS
videoTrackIndex = mMuxer.addTrack(videoOutputMediaFormat);
trackCnt++;
} else if (audioTrackIndex == null && type == FramePool.Frame.TYPE_AUDIO) {
MediaFormat audioOutputMediaFormat = params.getAudioOutputMediaFormat();
audioTrackIndex = mMuxer.addTrack(audioOutputMediaFormat);
trackCnt++;
}
if (trackCnt == 2) {
frameSender.sendStartMessage();
}
}
@Override
public int close() {
if (frameSender != null) {
frameSender.sendCloseMessage();
}
return 0;
}
}

@ -0,0 +1,132 @@
/*
*
* *
* * * Copyright (C) 2017 ChillingVan
* * *
* * * 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.xypower.stream.muxer;
import android.media.MediaCodec;
import android.text.TextUtils;
import com.chillingvan.canvasgl.util.Loggers;
import com.xypower.stream.publisher.StreamPublisher;
import net.butterflytv.rtmp_client.RTMPMuxer;
import java.util.Locale;
/**
* Created by Chilling on 2017/5/29.
*/
public class RTMPStreamMuxer extends BaseMuxer {
private RTMPMuxer rtmpMuxer;
private FrameSender frameSender;
public RTMPStreamMuxer() {
super();
}
/**
*
* @return 1 if it is connected
* 0 if it is not connected
*/
@Override
public synchronized int open(final StreamPublisher.StreamPublisherParam params) {
super.open(params);
if (TextUtils.isEmpty(params.outputUrl)) {
throw new IllegalArgumentException("Param outputUrl is empty");
}
rtmpMuxer = new RTMPMuxer();
// -2 Url format error; -3 Connect error.
int open = rtmpMuxer.open(params.outputUrl, params.width, params.height);
Loggers.d("RTMPStreamMuxer", String.format(Locale.CHINA, "open: open: %d", open));
boolean connected = rtmpMuxer.isConnected();
Loggers.d("RTMPStreamMuxer", String.format(Locale.CHINA, "open: isConnected: %b", connected));
Loggers.d("RTMPStreamMuxer", String.format("open: %s", params.outputUrl));
if (!TextUtils.isEmpty(params.outputFilePath)) {
rtmpMuxer.file_open(params.outputFilePath);
rtmpMuxer.write_flv_header(true, true);
}
frameSender = new FrameSender(new FrameSender.FrameSenderCallback() {
@Override
public void onStart() {}
@Override
public void onSendVideo(FramePool.Frame sendFrame) {
rtmpMuxer.writeVideo(sendFrame.data, 0, sendFrame.length, sendFrame.bufferInfo.getTotalTime());
}
@Override
public void onSendAudio(FramePool.Frame sendFrame) {
rtmpMuxer.writeAudio(sendFrame.data, 0, sendFrame.length, sendFrame.bufferInfo.getTotalTime());
}
@Override
public void close() {
if (rtmpMuxer != null) {
if (!TextUtils.isEmpty(params.outputFilePath)) {
rtmpMuxer.file_close();
}
rtmpMuxer.close();
rtmpMuxer = null;
}
}
});
frameSender.sendStartMessage();
return connected ? 1 : 0;
}
@Override
public void writeVideo(byte[] buffer, int offset, int length, MediaCodec.BufferInfo bufferInfo) {
super.writeVideo(buffer, offset, length, bufferInfo);
Loggers.d("RTMPStreamMuxer", "writeVideo: " + " time:" + videoTimeIndexCounter.getTimeIndex() + " offset:" + offset + " length:" + length);
frameSender.sendAddFrameMessage(buffer, offset, length, new BufferInfoEx(bufferInfo, videoTimeIndexCounter.getTimeIndex()), FramePool.Frame.TYPE_VIDEO);
}
@Override
public void writeAudio(byte[] buffer, int offset, int length, MediaCodec.BufferInfo bufferInfo) {
super.writeAudio(buffer, offset, length, bufferInfo);
Loggers.d("RTMPStreamMuxer", "writeAudio: ");
frameSender.sendAddFrameMessage(buffer, offset, length, new BufferInfoEx(bufferInfo, audioTimeIndexCounter.getTimeIndex()), FramePool.Frame.TYPE_AUDIO);
}
@Override
public synchronized int close() {
if (frameSender != null) {
frameSender.sendCloseMessage();
}
return 0;
}
}

@ -0,0 +1,48 @@
/*
*
* *
* * * Copyright (C) 2017 ChillingVan
* * *
* * * 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.xypower.stream.muxer;
/**
* Created by Chilling on 2017/12/23.
*/
public class TimeIndexCounter {
private long lastTimeUs;
private int timeIndex;
public void calcTotalTime(long currentTimeUs) {
if (lastTimeUs <= 0) {
this.lastTimeUs = currentTimeUs;
}
int delta = (int) (currentTimeUs - lastTimeUs);
this.lastTimeUs = currentTimeUs;
timeIndex += Math.abs(delta / 1000);
}
public void reset() {
lastTimeUs = 0;
timeIndex = 0;
}
public int getTimeIndex() {
return timeIndex;
}
}

@ -0,0 +1,126 @@
/*
*
* *
* * * Copyright (C) 2017 ChillingVan
* * *
* * * 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.xypower.stream.publisher;
import android.graphics.SurfaceTexture;
import com.chillingvan.canvasgl.glview.texture.GLMultiTexProducerView;
import com.chillingvan.canvasgl.glview.texture.GLTexture;
import com.chillingvan.canvasgl.glview.texture.gles.EglContextWrapper;
import com.chillingvan.canvasgl.glview.texture.gles.GLThread;
import com.xypower.stream.camera.CameraInterface;
import com.xypower.stream.encoder.video.H264Encoder;
import com.xypower.stream.muxer.IMuxer;
import java.io.IOException;
import java.util.List;
/**
* Data Stream:
* Camera -> SurfaceTexture of GLSurfaceTextureProducerView -> Surface of MediaCodec -> encode data(byte[]) -> RTMPMuxer -> Server
*
*/
public class CameraStreamPublisher {
private StreamPublisher streamPublisher;
private IMuxer muxer;
private GLMultiTexProducerView cameraPreviewTextureView;
private CameraInterface instantVideoCamera;
private OnSurfacesCreatedListener onSurfacesCreatedListener;
public CameraStreamPublisher(IMuxer muxer, GLMultiTexProducerView cameraPreviewTextureView, CameraInterface instantVideoCamera) {
this.muxer = muxer;
this.cameraPreviewTextureView = cameraPreviewTextureView;
this.instantVideoCamera = instantVideoCamera;
}
private void initCameraTexture() {
cameraPreviewTextureView.setOnCreateGLContextListener(new GLThread.OnCreateGLContextListener() {
@Override
public void onCreate(EglContextWrapper eglContext) {
streamPublisher = new StreamPublisher(eglContext, muxer);
}
});
cameraPreviewTextureView.setSurfaceTextureCreatedListener(new GLMultiTexProducerView.SurfaceTextureCreatedListener() {
@Override
public void onCreated(List<GLTexture> producedTextureList) {
if (onSurfacesCreatedListener != null) {
onSurfacesCreatedListener.onCreated(producedTextureList, streamPublisher);
}
GLTexture texture = producedTextureList.get(0);
SurfaceTexture surfaceTexture = texture.getSurfaceTexture();
streamPublisher.clearTextures();
streamPublisher.addSharedTexture(new GLTexture(texture.getRawTexture(), surfaceTexture));
surfaceTexture.setOnFrameAvailableListener(new SurfaceTexture.OnFrameAvailableListener() {
@Override
public void onFrameAvailable(SurfaceTexture surfaceTexture) {
cameraPreviewTextureView.requestRenderAndWait();
streamPublisher.drawAFrame();
}
});
instantVideoCamera.setPreview(surfaceTexture);
instantVideoCamera.startPreview();
}
});
}
public void prepareEncoder(StreamPublisher.StreamPublisherParam param, H264Encoder.OnDrawListener onDrawListener) {
streamPublisher.prepareEncoder(param, onDrawListener);
}
public void resumeCamera() {
if (instantVideoCamera.isOpened()) return;
instantVideoCamera.openCamera();
initCameraTexture();
cameraPreviewTextureView.onResume();
}
public boolean isStart() {
return streamPublisher != null && streamPublisher.isStart();
}
public void pauseCamera() {
if (!instantVideoCamera.isOpened()) return;
instantVideoCamera.release();
cameraPreviewTextureView.onPause();
}
public void startPublish() throws IOException {
streamPublisher.start();
}
public void closeAll() {
streamPublisher.close();
}
public void setOnSurfacesCreatedListener(OnSurfacesCreatedListener onSurfacesCreatedListener) {
this.onSurfacesCreatedListener = onSurfacesCreatedListener;
}
public interface OnSurfacesCreatedListener {
void onCreated(List<GLTexture> producedTextureList, StreamPublisher streamPublisher);
}
}

@ -0,0 +1,350 @@
/*
*
* *
* * * Copyright (C) 2017 ChillingVan
* * *
* * * 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.xypower.stream.publisher;
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaFormat;
import android.media.MediaRecorder;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Message;
import com.chillingvan.canvasgl.glview.texture.GLTexture;
import com.chillingvan.canvasgl.glview.texture.gles.EglContextWrapper;
import com.chillingvan.canvasgl.util.Loggers;
import com.xypower.stream.encoder.MediaCodecInputStream;
import com.xypower.stream.encoder.audio.AACEncoder;
import com.xypower.stream.encoder.video.H264Encoder;
import com.xypower.stream.muxer.IMuxer;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* Data Stream: <br>
* Video: <br>
* ... Something that can draw things on Surface(Original Surface) - > The shared Surface Texture
* -> The surface of MediaCodec -> encode data(byte[]) -> RTMPMuxer -> Server
* Audio: <br>
* MIC -> AudioRecord -> voice data(byte[]) -> MediaCodec -> encode data(byte[]) -> RTMPMuxer -> Server
*/
public class StreamPublisher {
public static final int MSG_OPEN = 1;
public static final int MSG_WRITE_VIDEO = 2;
private EglContextWrapper eglCtx;
private IMuxer muxer;
private AACEncoder aacEncoder;
private H264Encoder h264Encoder;
private boolean isStart;
private HandlerThread writeVideoHandlerThread;
private Handler writeVideoHandler;
private StreamPublisherParam param;
private List<GLTexture> sharedTextureList = new ArrayList<>();
public StreamPublisher(EglContextWrapper eglCtx, IMuxer muxer) {
this.eglCtx = eglCtx;
this.muxer = muxer;
}
public void prepareEncoder(final StreamPublisherParam param, H264Encoder.OnDrawListener onDrawListener) {
this.param = param;
try {
h264Encoder = new H264Encoder(param, eglCtx);
for (GLTexture texture :sharedTextureList ) {
h264Encoder.addSharedTexture(texture);
}
h264Encoder.setOnDrawListener(onDrawListener);
aacEncoder = new AACEncoder(param);
aacEncoder.setOnDataComingCallback(new AACEncoder.OnDataComingCallback() {
private byte[] writeBuffer = new byte[param.audioBitRate / 8];
@Override
public void onComing() {
MediaCodecInputStream mediaCodecInputStream = aacEncoder.getMediaCodecInputStream();
MediaCodecInputStream.readAll(mediaCodecInputStream, writeBuffer, new MediaCodecInputStream.OnReadAllCallback() {
@Override
public void onReadOnce(byte[] buffer, int readSize, MediaCodec.BufferInfo bufferInfo) {
if (readSize <= 0) {
return;
}
muxer.writeAudio(buffer, 0, readSize, bufferInfo);
}
});
}
});
} catch (IOException | IllegalStateException e) {
e.printStackTrace();
}
writeVideoHandlerThread = new HandlerThread("WriteVideoHandlerThread");
writeVideoHandlerThread.start();
writeVideoHandler = new Handler(writeVideoHandlerThread.getLooper()) {
private byte[] writeBuffer = new byte[param.videoBitRate / 8 / 2];
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
if (msg.what == MSG_WRITE_VIDEO) {
MediaCodecInputStream mediaCodecInputStream = h264Encoder.getMediaCodecInputStream();
MediaCodecInputStream.readAll(mediaCodecInputStream, writeBuffer, new MediaCodecInputStream.OnReadAllCallback() {
@Override
public void onReadOnce(byte[] buffer, int readSize, MediaCodec.BufferInfo bufferInfo) {
if (readSize <= 0) {
return;
}
Loggers.d("StreamPublisher", String.format("onReadOnce: %d", readSize));
muxer.writeVideo(buffer, 0, readSize, bufferInfo);
}
});
}
}
};
}
public void clearTextures() {
sharedTextureList.clear();
}
public void addSharedTexture(GLTexture outsideTexture) {
sharedTextureList.add(outsideTexture);
}
public void start() throws IOException {
if (!isStart) {
if (muxer.open(param) <= 0) {
Loggers.e("StreamPublisher", "muxer open fail");
throw new IOException("muxer open fail");
}
h264Encoder.start();
aacEncoder.start();
isStart = true;
}
}
public void close() {
isStart = false;
if (h264Encoder != null) {
h264Encoder.close();
}
if (aacEncoder != null) {
aacEncoder.close();
}
if (writeVideoHandlerThread != null) {
writeVideoHandlerThread.quitSafely();
}
if (muxer != null) {
muxer.close();
}
}
public boolean isStart() {
return isStart;
}
public boolean drawAFrame() {
if (isStart) {
h264Encoder.requestRender();
writeVideoHandler.sendEmptyMessage(MSG_WRITE_VIDEO);
return true;
}
return false;
}
public static class StreamPublisherParam {
public static final int DEFAULT_CHANNEL_CNT = 1;
public int width = 640;
public int height = 480;
public int videoBitRate = 2949120;
public int frameRate = 30;
public int iframeInterval = 5;
public int samplingRate;
public int audioBitRate;
public int audioSource;
public int channelCfg;
public int channelCnt;
public String videoMIMEType = "video/avc";
public String audioMIME = "audio/mp4a-latm";
public int audioBufferSize;
public String outputFilePath;
public String outputUrl;
private MediaFormat videoOutputMediaFormat;
private MediaFormat audioOutputMediaFormat;
private int initialTextureCount = 1;
public StreamPublisherParam() {
this(640, 480, 2949120, 30, 5, 44100, 32000, MediaRecorder.AudioSource.VOICE_COMMUNICATION, AudioFormat.CHANNEL_IN_MONO, DEFAULT_CHANNEL_CNT);
}
private StreamPublisherParam(int width, int height, int videoBitRate, int frameRate,
int iframeInterval, int samplingRate, int audioBitRate, int audioSource, int channelCfg, int channelCnt) {
this.width = width;
this.height = height;
this.videoBitRate = videoBitRate;
this.frameRate = frameRate;
this.iframeInterval = iframeInterval;
this.samplingRate = samplingRate;
this.audioBitRate = audioBitRate;
this.audioBufferSize = AudioRecord.getMinBufferSize(samplingRate, channelCfg, AudioFormat.ENCODING_PCM_16BIT) * 2;
this.audioSource = audioSource;
this.channelCfg = channelCfg;
this.channelCnt = channelCnt;
}
/**
*
* @param initialTextureCount Default is 1
*/
public void setInitialTextureCount(int initialTextureCount) {
if (initialTextureCount < 1) {
throw new IllegalArgumentException("initialTextureCount must >= 1");
}
this.initialTextureCount = initialTextureCount;
}
public int getInitialTextureCount() {
return initialTextureCount;
}
public MediaFormat createVideoMediaFormat() {
MediaFormat format = MediaFormat.createVideoFormat(videoMIMEType, width, height);
// Set some properties. Failing to specify some of these can cause the MediaCodec
// configure() call to throw an unhelpful exception.
format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
format.setInteger(MediaFormat.KEY_BIT_RATE, videoBitRate);
format.setInteger(MediaFormat.KEY_FRAME_RATE, frameRate);
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, iframeInterval);
return format;
}
public MediaFormat createAudioMediaFormat() {
MediaFormat format = MediaFormat.createAudioFormat(audioMIME, samplingRate, channelCnt);
format.setInteger(MediaFormat.KEY_BIT_RATE, audioBitRate);
format.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC);
format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, audioBufferSize);
return format;
}
public void setVideoOutputMediaFormat(MediaFormat videoOutputMediaFormat) {
this.videoOutputMediaFormat = videoOutputMediaFormat;
}
public void setAudioOutputMediaFormat(MediaFormat audioOutputMediaFormat) {
this.audioOutputMediaFormat = audioOutputMediaFormat;
}
public MediaFormat getVideoOutputMediaFormat() {
return videoOutputMediaFormat;
}
public MediaFormat getAudioOutputMediaFormat() {
return audioOutputMediaFormat;
}
public static class Builder {
private int width = 640;
private int height = 480;
private int videoBitRate = 2949120;
private int frameRate = 30;
private int iframeInterval = 5;
private int samplingRate = 44100;
private int audioBitRate = 32000;
private int audioSource = MediaRecorder.AudioSource.VOICE_COMMUNICATION;
private int channelCfg = AudioFormat.CHANNEL_IN_MONO;
private int channelCnt = 1;
public Builder setWidth(int width) {
this.width = width;
return this;
}
public Builder setHeight(int height) {
this.height = height;
return this;
}
public Builder setVideoBitRate(int videoBitRate) {
this.videoBitRate = videoBitRate;
return this;
}
public Builder setFrameRate(int frameRate) {
this.frameRate = frameRate;
return this;
}
public Builder setIframeInterval(int iframeInterval) {
this.iframeInterval = iframeInterval;
return this;
}
public Builder setSamplingRate(int samplingRate) {
this.samplingRate = samplingRate;
return this;
}
public Builder setAudioBitRate(int audioBitRate) {
this.audioBitRate = audioBitRate;
return this;
}
public Builder setAudioSource(int audioSource) {
this.audioSource = audioSource;
return this;
}
public Builder setChannelCfg(int channelCfg) {
this.channelCfg = channelCfg;
return this;
}
public void setChannelCnt(int channelCnt) {
this.channelCnt = channelCnt;
}
public StreamPublisherParam createStreamPublisherParam() {
return new StreamPublisherParam(width, height, videoBitRate, frameRate, iframeInterval, samplingRate, audioBitRate, audioSource, channelCfg, channelCnt);
}
}
}
}

@ -0,0 +1,23 @@
<!--
~ /*
~ *
~ * * Copyright (C) 2017 ChillingVan
~ * *
~ * * 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.
~ *
~ */
-->
<resources>
<string name="app_name">lib</string>
</resources>
Loading…
Cancel
Save