加入推流相关的依赖
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