diff --git a/app/build.gradle b/app/build.gradle
index 18b427c7..2fcb6542 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -26,7 +26,7 @@ android {
applicationId "com.xypower.mpapp"
minSdk COMPILE_MIN_SDK_VERSION as int
//noinspection ExpiredTargetSdkVersion
- targetSdk 28
+ targetSdk TARGET_SDK_VERSION as int
versionCode AppVersionCode
versionName AppVersionName
@@ -135,9 +135,10 @@ dependencies {
// implementation 'com.tencent:mmkv-static:1.3.0'
// implementation project(path: ':opencv')
implementation files('libs/devapi.aar')
- debugImplementation files('libs/rtmp-client-debug.aar')
- releaseImplementation files('libs/rtmp-client.aar')
- implementation project(':gpuv')
+ // debugImplementation files('libs/rtmp-client-debug.aar')
+ // releaseImplementation files('libs/rtmp-client.aar')
+ api project(':gpuv')
+ api project(':stream')
implementation 'dev.mobile:dadb:1.2.7'
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index f32efefc..5aadf097 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -72,6 +72,15 @@
+
+
+
+
+
+
-
-
+ android:grantUriPermissions="true" />
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/java/com/xypower/mpapp/BridgeService.java b/app/src/main/java/com/xypower/mpapp/BridgeService.java
deleted file mode 100644
index 37bf3278..00000000
--- a/app/src/main/java/com/xypower/mpapp/BridgeService.java
+++ /dev/null
@@ -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;
- }
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/xypower/mpapp/StreamActivity.java b/app/src/main/java/com/xypower/mpapp/StreamActivity.java
new file mode 100644
index 00000000..2ddf4e3d
--- /dev/null
+++ b/app/src/main/java/com/xypower/mpapp/StreamActivity.java
@@ -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;
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_stream.xml b/app/src/main/res/layout/activity_stream.xml
new file mode 100644
index 00000000..d0d7643a
--- /dev/null
+++ b/app/src/main/res/layout/activity_stream.xml
@@ -0,0 +1,9 @@
+
+
+
+
\ No newline at end of file
diff --git a/common/build.gradle b/common/build.gradle
index 04de2f89..931f0f37 100644
--- a/common/build.gradle
+++ b/common/build.gradle
@@ -6,8 +6,8 @@ android {
compileSdk 33
defaultConfig {
- minSdk 24
- targetSdk 28
+ minSdk COMPILE_MIN_SDK_VERSION as int
+ targetSdk TARGET_SDK_VERSION as int
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
diff --git a/mpmaster/build.gradle b/mpmaster/build.gradle
index 88c1deec..fb8e405c 100644
--- a/mpmaster/build.gradle
+++ b/mpmaster/build.gradle
@@ -14,8 +14,8 @@ android {
defaultConfig {
applicationId "com.xypower.mpmaster"
- minSdk 25
- targetSdk 28
+ minSdk COMPILE_MIN_SDK_VERSION as int
+ targetSdk TARGET_SDK_VERSION as int
versionCode AppVersionCode
versionName AppVersionName
diff --git a/settings.gradle b/settings.gradle
index 001b34d7..ac0b2a70 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -21,3 +21,4 @@ include ':gpuv'
// project(':opencv').projectDir = new File(opencvsdk + '/sdk')
include ':common'
+include ':stream'
\ No newline at end of file
diff --git a/stream/.gitignore b/stream/.gitignore
new file mode 100644
index 00000000..796b96d1
--- /dev/null
+++ b/stream/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/stream/build.gradle b/stream/build.gradle
new file mode 100644
index 00000000..646ab32b
--- /dev/null
+++ b/stream/build.gradle
@@ -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'
+}
\ No newline at end of file
diff --git a/app/libs/rtmp-client-debug.aar b/stream/libs/rtmp-client-debug.aar
similarity index 100%
rename from app/libs/rtmp-client-debug.aar
rename to stream/libs/rtmp-client-debug.aar
diff --git a/app/libs/rtmp-client.aar b/stream/libs/rtmp-client.aar
similarity index 100%
rename from app/libs/rtmp-client.aar
rename to stream/libs/rtmp-client.aar
diff --git a/stream/proguard-rules.pro b/stream/proguard-rules.pro
new file mode 100644
index 00000000..7f86d056
--- /dev/null
+++ b/stream/proguard-rules.pro
@@ -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 *;
+#}
diff --git a/stream/src/main/AndroidManifest.xml b/stream/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..764f77de
--- /dev/null
+++ b/stream/src/main/AndroidManifest.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
diff --git a/stream/src/main/java/com/xypower/stream/camera/CameraInterface.java b/stream/src/main/java/com/xypower/stream/camera/CameraInterface.java
new file mode 100644
index 00000000..53a3cc7f
--- /dev/null
+++ b/stream/src/main/java/com/xypower/stream/camera/CameraInterface.java
@@ -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();
+}
diff --git a/stream/src/main/java/com/xypower/stream/camera/CameraUtils.java b/stream/src/main/java/com/xypower/stream/camera/CameraUtils.java
new file mode 100644
index 00000000..43233463
--- /dev/null
+++ b/stream/src/main/java/com/xypower/stream/camera/CameraUtils.java
@@ -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.
+ *
+ * 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.
+ *
+ * It doesn't seem like there's a great deal of flexibility here.
+ *
+ * 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 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;
+ }
+}
diff --git a/stream/src/main/java/com/xypower/stream/camera/InstantVideoCamera.java b/stream/src/main/java/com/xypower/stream/camera/InstantVideoCamera.java
new file mode 100644
index 00000000..5babcb92
--- /dev/null
+++ b/stream/src/main/java/com/xypower/stream/camera/InstantVideoCamera.java
@@ -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;
+ }
+ }
+
+
+}
diff --git a/stream/src/main/java/com/xypower/stream/encoder/MediaCodecInputStream.java b/stream/src/main/java/com/xypower/stream/encoder/MediaCodecInputStream.java
new file mode 100644
index 00000000..1eb6a693
--- /dev/null
+++ b/stream/src/main/java/com/xypower/stream/encoder/MediaCodecInputStream.java
@@ -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);
+ }
+}
diff --git a/stream/src/main/java/com/xypower/stream/encoder/audio/AACEncoder.java b/stream/src/main/java/com/xypower/stream/encoder/audio/AACEncoder.java
new file mode 100644
index 00000000..e5f5d2ed
--- /dev/null
+++ b/stream/src/main/java/com/xypower/stream/encoder/audio/AACEncoder.java
@@ -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();
+ }
+
+}
diff --git a/stream/src/main/java/com/xypower/stream/encoder/video/H264Encoder.java b/stream/src/main/java/com/xypower/stream/encoder/video/H264Encoder.java
new file mode 100644
index 00000000..76d36360
--- /dev/null
+++ b/stream/src/main/java/com/xypower/stream/encoder/video/H264Encoder.java
@@ -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 producedTextures, List 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 producedTextures, List consumedTextures) {
+ if (onDrawListener != null) {
+ onDrawListener.onGLDraw(canvas, producedTextures, consumedTextures);
+ }
+ }
+
+ @Override
+ protected int getInitialTexCount() {
+ return initialTextureCount;
+ }
+ }
+}
diff --git a/stream/src/main/java/com/xypower/stream/muxer/BaseMuxer.java b/stream/src/main/java/com/xypower/stream/muxer/BaseMuxer.java
new file mode 100644
index 00000000..6cf7fae3
--- /dev/null
+++ b/stream/src/main/java/com/xypower/stream/muxer/BaseMuxer.java
@@ -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);
+ }
+}
diff --git a/stream/src/main/java/com/xypower/stream/muxer/BufferInfoEx.java b/stream/src/main/java/com/xypower/stream/muxer/BufferInfoEx.java
new file mode 100644
index 00000000..d935a2ca
--- /dev/null
+++ b/stream/src/main/java/com/xypower/stream/muxer/BufferInfoEx.java
@@ -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;
+ }
+}
diff --git a/stream/src/main/java/com/xypower/stream/muxer/FramePool.java b/stream/src/main/java/com/xypower/stream/muxer/FramePool.java
new file mode 100644
index 00000000..6737644d
--- /dev/null
+++ b/stream/src/main/java/com/xypower/stream/muxer/FramePool.java
@@ -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 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 {
+
+ 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 frameQueue) {
+ Collections.sort(frameQueue, new Comparator() {
+ @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;
+ }
+ }
+ });
+ }
+ }
+}
diff --git a/stream/src/main/java/com/xypower/stream/muxer/FrameSender.java b/stream/src/main/java/com/xypower/stream/muxer/FrameSender.java
new file mode 100644
index 00000000..6f88ca8b
--- /dev/null
+++ b/stream/src/main/java/com/xypower/stream/muxer/FrameSender.java
@@ -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 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();
+
+ }
+}
diff --git a/stream/src/main/java/com/xypower/stream/muxer/IMuxer.java b/stream/src/main/java/com/xypower/stream/muxer/IMuxer.java
new file mode 100644
index 00000000..94748a9f
--- /dev/null
+++ b/stream/src/main/java/com/xypower/stream/muxer/IMuxer.java
@@ -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();
+}
diff --git a/stream/src/main/java/com/xypower/stream/muxer/MP4Muxer.java b/stream/src/main/java/com/xypower/stream/muxer/MP4Muxer.java
new file mode 100644
index 00000000..5d07325f
--- /dev/null
+++ b/stream/src/main/java/com/xypower/stream/muxer/MP4Muxer.java
@@ -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;
+ }
+}
diff --git a/stream/src/main/java/com/xypower/stream/muxer/RTMPStreamMuxer.java b/stream/src/main/java/com/xypower/stream/muxer/RTMPStreamMuxer.java
new file mode 100644
index 00000000..0208f21e
--- /dev/null
+++ b/stream/src/main/java/com/xypower/stream/muxer/RTMPStreamMuxer.java
@@ -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;
+ }
+
+
+}
diff --git a/stream/src/main/java/com/xypower/stream/muxer/TimeIndexCounter.java b/stream/src/main/java/com/xypower/stream/muxer/TimeIndexCounter.java
new file mode 100644
index 00000000..922b949c
--- /dev/null
+++ b/stream/src/main/java/com/xypower/stream/muxer/TimeIndexCounter.java
@@ -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;
+ }
+}
diff --git a/stream/src/main/java/com/xypower/stream/publisher/CameraStreamPublisher.java b/stream/src/main/java/com/xypower/stream/publisher/CameraStreamPublisher.java
new file mode 100644
index 00000000..ec82a6bd
--- /dev/null
+++ b/stream/src/main/java/com/xypower/stream/publisher/CameraStreamPublisher.java
@@ -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 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 producedTextureList, StreamPublisher streamPublisher);
+ }
+}
diff --git a/stream/src/main/java/com/xypower/stream/publisher/StreamPublisher.java b/stream/src/main/java/com/xypower/stream/publisher/StreamPublisher.java
new file mode 100644
index 00000000..a285436d
--- /dev/null
+++ b/stream/src/main/java/com/xypower/stream/publisher/StreamPublisher.java
@@ -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:
+ * Video:
+ * ... Something that can draw things on Surface(Original Surface) - > The shared Surface Texture
+ * -> The surface of MediaCodec -> encode data(byte[]) -> RTMPMuxer -> Server
+ * Audio:
+ * 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 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);
+ }
+ }
+ }
+
+}
diff --git a/stream/src/main/res/values/strings.xml b/stream/src/main/res/values/strings.xml
new file mode 100644
index 00000000..d90dd0b6
--- /dev/null
+++ b/stream/src/main/res/values/strings.xml
@@ -0,0 +1,23 @@
+
+
+
+ lib
+