加入推流相关的依赖
parent
34c6f8c341
commit
56538fec1a
@ -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>
|
@ -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…
Reference in New Issue