diff --git a/.idea/gradle.xml b/.idea/gradle.xml index 144e8f4..3064d5f 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -25,7 +25,6 @@ diff --git a/app/build.gradle b/app/build.gradle index bf15055..6c34267 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -42,6 +42,8 @@ dependencies { implementation rootProject.ext.dependencies.androidxconstraintlayout implementation rootProject.ext.dependencies.rxjava implementation rootProject.ext.dependencies.rxandroid + // 从车机获取视频流 + implementation 'com.zhidao.carmanager:common:1.0.23@aar' if (Boolean.valueOf(RELEASE)) { implementation "com.mogo.cloud:tanlu:${MOGO_TANLU_VERSION}" @@ -49,6 +51,7 @@ dependencies { } else { implementation project(":modules:mogo-tanlu") implementation project(":modules:mogo-realtime") + implementation project(":foudations:mogo-live") } annotationProcessor 'com.elegant.spi:compiler:1.0.3' //编译时库 diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c3952e7..cb61520 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,21 @@ + + + + + + + + + + + + + - - - - + android:name=".PushActivity" + android:label="推送直播" /> - - - + android:label="网络测试" /> - - - + android:label="实时数据测试" /> - + android:label="路况服务" /> diff --git a/app/src/main/java/com/mogo/cloud/BaseLiveActivity.java b/app/src/main/java/com/mogo/cloud/BaseLiveActivity.java new file mode 100644 index 0000000..de3620e --- /dev/null +++ b/app/src/main/java/com/mogo/cloud/BaseLiveActivity.java @@ -0,0 +1,148 @@ +package com.mogo.cloud; + +import android.os.Bundle; +import android.util.Log; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.widget.Toast; +import android.widget.ToggleButton; + +import androidx.appcompat.app.AppCompatActivity; + +import com.mogo.cloud.util.YuvToolUtils; +import com.zhidao.manager.camera.FrameBufferCallBack; +import com.zhidao.manager.camera.ZDCameraManager; +import com.zhidao.manager.camera.ZDCameraParams; + + +public abstract class BaseLiveActivity extends AppCompatActivity { + private String TAG = "TestCarRecorderLiveActivity"; + + private String yuvSavePath = "/sdcard/Movies/TestYuvNV12.yuv"; + + public static int cameraId = 0; // 要打开的摄像头的ID + + public int videoWidth = 1280; + public int videoHeight = 720; + + // 开始直播 + protected ToggleButton btnLive; + // 保存文件到本地 + private ToggleButton btnSaveFile; + + // 相机数据预览 + protected SurfaceView surfaceView; + + private ZDCameraManager zdCameraManager; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_push_video); + + surfaceView = findViewById(R.id.surfaceView); + btnLive = findViewById(R.id.btnLive); + btnLive.setOnCheckedChangeListener((buttonView, isChecked) -> { + Toast.makeText(getApplicationContext(), buttonView.getText(), Toast.LENGTH_SHORT).show(); + toggleLive(isChecked); + }); + + btnSaveFile = findViewById(R.id.btnSaveFile); + btnSaveFile.setOnCheckedChangeListener((btnSaveFile, isChecked) -> { + Toast.makeText(getApplicationContext(), btnSaveFile.getText(), Toast.LENGTH_SHORT).show(); + YuvToolUtils.isSaveFile = isChecked; + if (isChecked) { + YuvToolUtils.connectYUVFile(yuvSavePath); + } + }); + + initCamer(); + } + + /** + * 初始化相机 + */ + private void initCamer() { + zdCameraManager = ZDCameraManager.getInstance(); + zdCameraManager.init(getApplicationContext()); + ZDCameraParams camParam = zdCameraManager.getCameraParam(); + camParam.setStoragePath("/sdcard/DCIM/"); + camParam.setVideoWidth(0, videoWidth); + camParam.setVideoHeight(0, videoHeight); + zdCameraManager.setCameraParam(camParam); + + // 这里是获取 YUV-NV12 格式的视频流 + ZDCameraManager.getInstance().setFrontBufferCallBack(new FrameBufferCallBack() { + @Override + public void onFrame(byte[] bytes, int i) { + //Log.d(TAG, "duanmu OnFrame ,bytes:" + bytes + " i:" + i); + // 回碉给业务侧进行处理 + // 这里对所有传入的YUV数据进行对应类型的转码,为I420 + byte[] yuv420p = YuvToolUtils.convertData(bytes, videoWidth, videoHeight, 4); + + onVideoFrame(yuv420p, i); + + // 保存yuv文件 + if (YuvToolUtils.isSaveFile) { + try { + // 往队列里面添加数据 + YuvToolUtils.queueYUV.put(yuv420p); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + }); + + // 这里是纯预览 + surfaceView.getHolder().addCallback(new SurfaceHolder.Callback() { + @Override + public void surfaceCreated(SurfaceHolder holder) { + Log.i(TAG, "addSurfaceCallBack id"); + zdCameraManager.startPreview(holder, cameraId, videoWidth, videoHeight); + } + + @Override + public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { + } + + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + zdCameraManager.stopPreview(cameraId); + } + }); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + if (zdCameraManager != null) { + zdCameraManager.stopPreview(cameraId); + } + } + + /** + * F 车机从相机获取的视频数据回调 + *

+ * TODO 一般在这里调用直播SDK的接口进行直播操作,一定要先卸载 ADAS com.zhidao.autopilot + * + * @param yuv420p YUV数据 I420 + * @param bytesLength 数据长度 + */ + public abstract void onVideoFrame(byte[] yuv420p, int bytesLength); + + + /** + * 开关直播状态 + * + * @param isLive true-开启直播,false-关闭直播 + */ + public abstract void toggleLive(boolean isLive); + + /** + * 是否是静音模式 + * + * @param isMute true-静音,false-非静音 + */ + public abstract void toggleMute(boolean isMute); +} \ No newline at end of file diff --git a/app/src/main/java/com/mogo/cloud/MainActivity.java b/app/src/main/java/com/mogo/cloud/MainActivity.java index 29a3de1..53dbb26 100644 --- a/app/src/main/java/com/mogo/cloud/MainActivity.java +++ b/app/src/main/java/com/mogo/cloud/MainActivity.java @@ -18,6 +18,7 @@ public class MainActivity extends AppCompatActivity { private Button btnJumpNetWorkPort; private Button btnJumpRealTime; private Button btnJumpRoadCondition; + private Button btnJumpPushLive; private TextView tvSn; private TextView tvToken; @@ -57,6 +58,12 @@ public class MainActivity extends AppCompatActivity { startActivity(intent); }); + btnJumpPushLive = findViewById(R.id.btnJumpPushLive); + btnJumpPushLive.setOnClickListener(v -> { + Intent intent = new Intent(MainActivity.this, PushActivity.class); + startActivity(intent); + }); + MoGoAiCloudClient.getInstance().addTokenCallbacks(new IMoGoTokenCallback() { @Override public void onTokenGot(String token, String sn) { diff --git a/app/src/main/java/com/mogo/cloud/PushActivity.java b/app/src/main/java/com/mogo/cloud/PushActivity.java new file mode 100644 index 0000000..00f2946 --- /dev/null +++ b/app/src/main/java/com/mogo/cloud/PushActivity.java @@ -0,0 +1,120 @@ +package com.mogo.cloud; + +import android.media.AudioFormat; +import android.os.Bundle; +import android.os.SystemClock; +import android.util.Log; + +import com.mogo.cloud.live.listener.ILiveProgressListener; +import com.mogo.cloud.live.manager.MGLivePushConfig; +import com.mogo.cloud.live.manager.ZeGoLiveManager; +import com.mogo.cloud.live.utils.ByteUtils; +import com.mogo.cloud.util.Devices; + + +/** + * 推流页面 + */ +public class PushActivity extends BaseLiveActivity { + public static final String TAG = "PushActivity"; + + private boolean isLoginSuccess = false; + private boolean isLive = false; + private boolean isOnStart = false; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + MGLivePushConfig mLivePushConfig = new MGLivePushConfig(); + mLivePushConfig.setWidth(1280); + mLivePushConfig.setHeight(720); + mLivePushConfig.setVideoBitrate(1500); + mLivePushConfig.setVideoFPS(15); + mLivePushConfig.setAudioChannels(2); + mLivePushConfig.setAudioSampleRate(44100); + mLivePushConfig.setAudioFormat(AudioFormat.ENCODING_PCM_16BIT); + mLivePushConfig.setMute(true); + + ZeGoLiveManager.getInstance().init(this.getApplication(), mLivePushConfig); + ZeGoLiveManager.getInstance().setLiveProgressListener(new ILiveProgressListener() { + @Override + public void onStart() { + Log.i(TAG, "onStart"); + isOnStart = true; + } + + @Override + public void onStop() { + Log.i(TAG, "onStop"); + isOnStart = false; + } + + @Override + public void onConnecting() { + Log.i(TAG, "onConnecting"); + } + + @Override + public void onConnected(String roomId) { + Log.i(TAG, "onConnected"); + isLoginSuccess = true; + } + + @Override + public void onDisConnect() { + Log.i(TAG, "onDisConnect"); + isLoginSuccess = false; + } + + @Override + public void onDebugError(int errorCode, String funcName, String errorInfo) { + Log.i(TAG, "errorCode : " + errorCode + " , funcName : " + funcName + " , errorInfo : " + errorInfo); + } + }); + } + + @Override + public void onVideoFrame(byte[] bytes, int bytesLength) { + if (!isLoginSuccess) { +// Log.i(TAG, "还未进房成功"); + return; + } + if (!isOnStart) { +// Log.i(TAG, "还未执行onStart回调"); + return; + } + if (!isLive) { + return; + } + Log.i(TAG, "onVideoFrame byte length: " + bytesLength); + ZeGoLiveManager.getInstance().startPublishingStream(ByteUtils.getBuffer(bytes, bytesLength), bytesLength, SystemClock.elapsedRealtime()); + } + + @Override + public void toggleLive(boolean isLive) { + this.isLive = isLive; + if (!isLoginSuccess) { + Log.i(TAG, "toggleLive isLive : " + isLive); + btnLive.setChecked(!isLive); + return; + } + Log.i(TAG, "toggleLive : " + isLive); + if (isLive) { + ZeGoLiveManager.getInstance().startPush(Devices.getSn()); + } else { + ZeGoLiveManager.getInstance().stopPublish(); + } + } + + @Override + public void toggleMute(boolean isMute) { + + } + + @Override + protected void onDestroy() { + super.onDestroy(); + ZeGoLiveManager.getInstance().onDestroyPublish(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/mogo/cloud/util/Devices.java b/app/src/main/java/com/mogo/cloud/util/Devices.java new file mode 100644 index 0000000..aea09c5 --- /dev/null +++ b/app/src/main/java/com/mogo/cloud/util/Devices.java @@ -0,0 +1,33 @@ +package com.mogo.cloud.util; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +public class Devices { + + private static final String PROPERTIES = "android.os.SystemProperties"; + private static final String GSM_SERIAL = "gsm.serial"; + private static final String GET = "get"; + + public static String getSn(){ + return getSystemProperties(GSM_SERIAL); + } + + public static String getSystemProperties(String name ) { + String value = ""; + try { + Class< ? > c = Class.forName( PROPERTIES ); + Method get = c.getMethod( GET, String.class ); + value = (String) get.invoke( c, name ); + } catch ( ClassNotFoundException var3 ) { + var3.printStackTrace(); + } catch ( NoSuchMethodException var4 ) { + var4.printStackTrace(); + } catch ( InvocationTargetException var5 ) { + var5.printStackTrace(); + } catch ( IllegalAccessException var6 ) { + var6.printStackTrace(); + } + return value; + } +} diff --git a/app/src/main/java/com/mogo/cloud/util/YuvToolUtils.java b/app/src/main/java/com/mogo/cloud/util/YuvToolUtils.java new file mode 100644 index 0000000..ee21ada --- /dev/null +++ b/app/src/main/java/com/mogo/cloud/util/YuvToolUtils.java @@ -0,0 +1,197 @@ +package com.mogo.cloud.util; + +import android.util.Log; + +import com.zhidao.libyuv.Key; + +import java.io.File; +import java.io.FileDescriptor; +import java.io.FileOutputStream; +import java.util.ArrayList; +import java.util.concurrent.LinkedBlockingQueue; + +/** + * YUV与RGB转换工具类 + */ +public class YuvToolUtils { + private static final String TAG = "YuvToolUtils"; + + private static final int DST_WIDTH = 1280; + private static final int DST_HEIGHT = 720; + + private static byte[] dst = null; + private static byte[] tmp = null; + + /** + * @param data origin yuv data + * @param width data width + * @param height data height + * @param type 1:YV12 2:NV21 3:I420 4 nv12 + */ + public static byte[] convertData(byte[] data, int width, int height, int type) { + if (dst == null) { + dst = new byte[DST_WIDTH * DST_HEIGHT * 3 / 2]; + } + + if (tmp == null) { + tmp = new byte[848 * 480 * 3 / 2]; + } + + if (type == 4) { + if (DST_WIDTH == width && DST_HEIGHT == height) { + com.zhidao.libyuv.YuvUtils.NV12ToI420(data, dst, width, height); + return dst; + } else { + com.zhidao.libyuv.YuvUtils.NV12ToI420(data, tmp, width, height); + com.zhidao.libyuv.YuvUtils.I420Scale(tmp, width, height, dst, DST_WIDTH, DST_HEIGHT, Key.SCALE_MODE_NONE, false); + return dst; + } + } else if (type == 3) { + if (DST_WIDTH == width && DST_HEIGHT == height) { + return data; + } else { + com.zhidao.libyuv.YuvUtils.I420Scale(data, width, height, dst, DST_WIDTH, DST_HEIGHT, Key.SCALE_MODE_NONE, false); + return dst; + } + } else if (type == 2) { + if (DST_WIDTH == width && DST_HEIGHT == height) { + com.zhidao.libyuv.YuvUtils.NV21ToI420(data, dst, width, height, false); + return dst; + } else { + com.zhidao.libyuv.YuvUtils.NV21ToI420(data, tmp, width, height, false); + com.zhidao.libyuv.YuvUtils.I420Scale(tmp, width, height, dst, DST_WIDTH, DST_HEIGHT, Key.SCALE_MODE_NONE, false); + return dst; + } + } else if (type == 1) { + if (DST_WIDTH == width && DST_HEIGHT == height) { + swapYV12toI420(data, dst, width, height); + return dst; + } else { + swapYV12toI420(data, tmp, width, height); + com.zhidao.libyuv.YuvUtils.I420Scale(tmp, width, height, dst, DST_WIDTH, DST_HEIGHT, Key.SCALE_MODE_NONE, false); + return dst; + } + } + return new byte[0]; + } + + public static synchronized void release() { + dst = null; + tmp = null; + Log.d(TAG, "release 释放临时帧"); + } + + /** + * 将 YV12 格式转换为 I420 + * + * @param yv12bytes 原始格式数据 + * @param i420bytes 转出格式数据 + * @param width 宽度 + * @param height 高度 + */ + public static void swapYV12toI420(byte[] yv12bytes, byte[] i420bytes, int width, int height) { + System.arraycopy(yv12bytes, 0, i420bytes, 0, width * height); + int srcPos = width * height + width * height / 4; + // 这里利用的是 YV12 和 I420 存储特性 ,将 U 和 V 数据进行调换 + System.arraycopy(yv12bytes, srcPos, i420bytes, width * height, width * height / 4); + System.arraycopy(yv12bytes, width * height, i420bytes, srcPos, width * height / 4); + } + + + public static ArrayList splitYUVI420(byte[] i420bytes, int width, int height) { + + ArrayList yuvList = new ArrayList<>(); + + byte[] yData = new byte[width * height]; + byte[] uData = new byte[width * height / 4]; + byte[] vData = new byte[width * height / 4]; + + System.arraycopy(i420bytes, 0, yData, 0, width * height); + System.arraycopy(i420bytes, width * height, uData, 0, width * height / 4); + System.arraycopy(i420bytes, width * height + width * height / 4, vData, 0, width * height / 4); + + yuvList.add(yData); + yuvList.add(uData); + yuvList.add(vData); + + return yuvList; + } + + + public static LinkedBlockingQueue queueYUV = new LinkedBlockingQueue<>(); + public static boolean isSaveFile = true; + + public static void connectYUVFile(String filePath) { + Thread writeThread = new Thread(new Runnable() { + @Override + public void run() { + try { + File file = new File(filePath); + if (!file.exists()) { + boolean isOk = file.createNewFile(); + if (isOk) { + Log.d(TAG, filePath + " 文件创建成功"); + } else { + Log.w(TAG, filePath + " 文件已经存在"); + } + } + FileOutputStream out = new FileOutputStream(filePath); + // 子线程中死循环 + while (isSaveFile) { + // 拿数据 + byte[] data = queueYUV.take(); + // 往本地文件写入 + out.write(data); + // 刷新 + out.flush(); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + }); + writeThread.start(); + } + + + /** + * 写入文件 + * + * @param path 文件路径 + * @param data YUV 数据 + * @return 是否写入成功 + */ + public static boolean writeFile(String path, byte[] data) { + FileOutputStream out = null; + try { + File file = new File(path); + File parent = file.getParentFile(); + if (parent != null && !parent.exists()) + parent.mkdirs(); + if (!file.exists()) { + boolean isOk = file.createNewFile(); + if (isOk) { + out = new FileOutputStream(path); + out.write(data); + FileDescriptor fd = out.getFD(); + fd.sync(); + } + return true; + } else { + Log.d(TAG, path + " 文件已经存在"); + } + } catch (Exception e) { + e.printStackTrace(); + } finally { + try { + if (out != null) + out.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } + return false; + } + + +} diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index b61d77c..b32e84a 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -65,6 +65,12 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:text="路况服务测试" /> + +