3个文件已添加
9个文件已修改
1415 ■■■■■ 已修改文件
.gitignore 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/.gitignore 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/AndroidManifest.xml 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/safeluck/floatwindow/FloatingService.java 108 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/safeluck/floatwindow/MainActivity.kt 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/safeluck/floatwindow/MediaArgu.java 23 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/safeluck/floatwindow/manager/UsbCameraPushManager.java 457 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/safeluck/floatwindow/manager/UsbCameraRecordManager.java 139 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/safeluck/floatwindow/util/AudioRecordManager.java 123 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
app/src/main/java/com/safeluck/floatwindow/util/GlobalData.java 128 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
readMe.md 375 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
usbcameralib/src/main/cpp/watermark.cpp 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
.gitignore
@@ -13,3 +13,52 @@
.externalNativeBuild
.cxx
local.properties
/usbcameralib/.cxx/
/.kotlin/
# Build directories
build/
**/build/
**/.cxx/
**/.kotlin/
**/.gradle/
**/build/
**/intermediates/
**/generated/
**/outputs/
**/.externalNativeBuild/
# Android Studio
*.iml
.idea/
.gradle/
local.properties
*.apk
*.ap_
*.aab
# NDK
obj/
.externalNativeBuild/
.cxx/
# Kotlin
.kotlin/
# Generated files
*.class
*.dex
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar
# Log files
*.log
# OS files
.DS_Store
Thumbs.db
app/.gitignore
@@ -1 +1,2 @@
/build
/build
/build/
app/src/main/AndroidManifest.xml
@@ -34,6 +34,12 @@
            android:name=".FloatingService"
            android:enabled="true"
            android:exported="false" />
        <service
            android:name=".P2UsbCameraVideoService"
            android:enabled="true"
            android:exported="false"
            android:process=":p2" />
    </application>
</manifest>
app/src/main/java/com/safeluck/floatwindow/FloatingService.java
@@ -1,7 +1,9 @@
package com.safeluck.floatwindow;
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.ServiceConnection;
import android.content.Intent;
import android.os.IBinder;
import android.os.RemoteCallbackList;
@@ -32,6 +34,50 @@
    
    // 当前使用的管理器类型
    private ManagerType currentManagerType = ManagerType.NONE;
    // P2 跨进程服务(用于 usbCameraId == 2)
    private IMediaAidlInterface p2Service;
    private boolean p2Bound = false;
    private MediaArgu pendingP2StartMedia;
    private final IMyCallback p2Callback = new IMyCallback.Stub() {
        @Override
        public void onResult(ResponseVO re) throws RemoteException {
            // 将 P2 进程回调转发给客户端
            notifyCallback(re);
        }
    };
    private final ServiceConnection p2Connection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            p2Service = IMediaAidlInterface.Stub.asInterface(service);
            p2Bound = true;
            Timber.d("P2UsbCameraVideoService connected");
            try {
                p2Service.registerCallback(p2Callback);
            } catch (RemoteException e) {
                Timber.e(e, "Failed to register p2Callback");
            }
            // 如果有 pending startMedia,连接后立刻执行
            if (pendingP2StartMedia != null) {
                try {
                    p2Service.startMedia(pendingP2StartMedia);
                    pendingP2StartMedia = null;
                } catch (RemoteException e) {
                    Timber.e(e, "Failed to startMedia on P2 service");
                }
            }
        }
        @Override
        public void onServiceDisconnected(ComponentName name) {
            Timber.w("P2UsbCameraVideoService disconnected");
            p2Bound = false;
            p2Service = null;
        }
    };
    
    /**
     * 管理器类型枚举
@@ -40,7 +86,9 @@
        NONE,
        USB_PUSH,
        USB_RECORD,
        ANDROID_RECORD
        ANDROID_RECORD,
        P2_USB_PUSH,
        P2_USB_RECORD
    }
    
    // AIDL Binder
@@ -116,6 +164,16 @@
        
        Timber.d("FloatingService onCreate");
    }
    private void ensureP2Bound() {
        if (p2Bound) return;
        Intent intent = new Intent(this, P2UsbCameraVideoService.class);
        try {
            bindService(intent, p2Connection, Context.BIND_AUTO_CREATE);
        } catch (Exception e) {
            Timber.e(e, "bindService P2UsbCameraVideoService failed");
        }
    }
    
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
@@ -129,6 +187,26 @@
        if (media == null) {
            Timber.w("startMedia: media is null");
            notifyCallback(1, -1, "MediaArgu is null");
            return;
        }
        // usbCameraId == 2:走 P2 跨进程服务,支持两路 USB 同时工作
        if (media.isUsedOutCamera() && media.getUsbCameraId() == 2) {
            stopCurrentManager();
            ensureP2Bound();
            if (p2Service != null) {
                try {
                    p2Service.startMedia(media);
                    currentManagerType = media.isPush() ? ManagerType.P2_USB_PUSH : ManagerType.P2_USB_RECORD;
                } catch (RemoteException e) {
                    Timber.e(e, "startMedia forward to P2 failed");
                    notifyCallback(1, -3, "启动P2服务失败: " + e.getMessage());
                }
            } else {
                // 等待连接完成后执行
                pendingP2StartMedia = media;
                currentManagerType = media.isPush() ? ManagerType.P2_USB_PUSH : ManagerType.P2_USB_RECORD;
            }
            return;
        }
        
@@ -183,6 +261,18 @@
                    androidCameraRecordManager.stopRecord();
                }
                break;
            case P2_USB_PUSH:
            case P2_USB_RECORD:
                if (p2Service != null) {
                    try {
                        p2Service.stopMedia();
                    } catch (RemoteException e) {
                        Timber.e(e, "stopMedia forward to P2 failed");
                    }
                } else {
                    pendingP2StartMedia = null;
                }
                break;
            case NONE:
                break;
        }
@@ -208,6 +298,22 @@
        super.onDestroy();
        stopMedia();
        mCallbacks.kill();
        if (p2Bound) {
            try {
                if (p2Service != null) {
                    p2Service.unregisterCallback(p2Callback);
                }
            } catch (RemoteException e) {
                Timber.e(e, "Failed to unregister p2Callback");
            }
            try {
                unbindService(p2Connection);
            } catch (Exception e) {
                Timber.w(e, "unbindService P2 failed");
            }
            p2Bound = false;
            p2Service = null;
        }
        Timber.d("FloatingService onDestroy");
    }
    
app/src/main/java/com/safeluck/floatwindow/MainActivity.kt
@@ -178,7 +178,7 @@
                codeRate = 0
                frameRate = 0
                m_screen = MediaArgu.ScreenSolution(640, 480) // 默认分辨率
                url = "rtmp://your-push-url" // TODO: 需要设置实际的推流地址
                url = "rtmp://192.168.16.143/live/livestream" // TODO: 需要设置实际的推流地址
                userName = ""
                pwd = ""
            }
app/src/main/java/com/safeluck/floatwindow/MediaArgu.java
@@ -14,8 +14,8 @@
    private boolean isPush;//是否推流 true-是
    private boolean usedOutCamera;//默认false 使用内置摄像头, true使用外置摄像头
    private int usbCameraId;//标记是用1- P1 usb摄像头  2-P2摄像头
    private String jsonconfig ; //以后可能会用到,目前用不到
    private int codeRate = 0;// 码率
    private int frameRate = 0;// 帧率
    private ScreenSolution m_screen;
@@ -24,8 +24,23 @@
    private String pwd;//ftp上传密码
    private int recordTime;//分钟
    public int getUsbCameraId() {
        return usbCameraId;
    }
private int tfCardFlag =0; //0-内部flash 1- 外置tfcard
    public String getJsonconfig() {
        return jsonconfig;
    }
    public void setJsonconfig(String jsonconfig) {
        this.jsonconfig = jsonconfig;
    }
    public void setUsbCameraId(int usbCameraId) {
        this.usbCameraId = usbCameraId;
    }
    private int tfCardFlag =0; //0-内部flash 1- 外置tfcard
    public int getTfCardFlag() {
        return tfCardFlag;
@@ -181,6 +196,7 @@
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeByte(this.isPush ? (byte) 1 : (byte) 0);
        dest.writeByte(this.usedOutCamera ? (byte) 1 : (byte) 0);
        dest.writeInt(this.usbCameraId);
        dest.writeInt(this.codeRate);
        dest.writeInt(this.frameRate);
@@ -198,6 +214,7 @@
    protected MediaArgu(Parcel in) {
        this.isPush = in.readByte() != 0;
        this.usedOutCamera = in.readByte() != 0;
        this.usbCameraId = in.readInt();
        this.codeRate = in.readInt();
        this.frameRate = in.readInt();
        this.m_screen = in.readParcelable(ScreenSolution.class.getClassLoader());
app/src/main/java/com/safeluck/floatwindow/manager/UsbCameraPushManager.java
@@ -1,6 +1,22 @@
package com.safeluck.floatwindow.manager;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.WindowManager;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import com.alivc.live.pusher.AlivcAudioAACProfileEnum;
import timber.log.Timber;
@@ -19,56 +35,67 @@
import com.alivc.live.pusher.AlivcQualityModeEnum;
import com.alivc.live.pusher.AlivcResolutionEnum;
import com.anyun.libusbcamera.UsbCamera;
import com.anyun.libusbcamera.WatermarkParam;
import com.safeluck.floatwindow.MediaArgu;
import com.safeluck.floatwindow.ResponseVO;
import com.safeluck.floatwindow.util.AudioRecordManager;
import com.safeluck.floatwindow.util.GlobalData;
/**
 * USB摄像头推流管理器
 */
public class UsbCameraPushManager {
    private static final String TAG = "UsbCameraPushManager";
    private Context context;
    private MediaArgu mediaArgu;
    private PushCallback callback;
    // 阿里推流相关
    private AlivcLivePusher alivcPusher;
    private AlivcLivePushConfig alivcLivePushConfig;
    private SurfaceView previewSurfaceView;
    // USB摄像头相关
    private UsbCamera usbCamera;
    private PushThread pushThread;
    private boolean isRunning = false;
    private boolean cameraExists = false;
    private volatile boolean pushStarted = false;
    // 推流URL
    private String pushUrl;
    // 分辨率数组 [width, height]
    private int[] resolutionArr = new int[]{640, 480};
    // 是否开启摄像头加密
    private boolean ay_encrypt = false;
    // 预览 SurfaceView 和隐藏的 Window
    private WindowManager windowManager;
    // 音频推流线程池(单线程)
    private ExecutorService audioPushExecutor;
    /**
     * 推流回调接口
     */
    public interface PushCallback {
        void onResult(ResponseVO response);
    }
    public UsbCameraPushManager(Context context) {
        this.context = context;
    }
    /**
     * 设置回调
     */
    public void setCallback(PushCallback callback) {
        this.callback = callback;
    }
    /**
     * 开始推流
     */
@@ -77,111 +104,287 @@
            notifyCallback(1, -1, "MediaArgu is null");
            return;
        }
        this.mediaArgu = media;
        this.pushUrl = media.getUrl();
        if (pushUrl == null || pushUrl.isEmpty()) {
            notifyCallback(1, -2, "Push URL is empty");
            return;
        }
        // 设置分辨率
        if (media.getM_screen() != null) {
            resolutionArr[0] = media.getM_screen().getWidth();
            resolutionArr[1] = media.getM_screen().getHeight();
            Timber.d("设置分辨率: %dx%d", resolutionArr[0], resolutionArr[1]);
        }
        try {
            // 初始化推流SDK
            initAlivcPusher();
            setWaterMask();
            pushStarted = false;
            // 检查并打开USB摄像头
            if (!openUsbCamera()) {
                cameraExists = false;
                notifyCallback(1, -1, "USB摄像头打开失败");
                return;
            }
            cameraExists = true;
            Timber.d("USB摄像头打开成功");
            // 启动摄像头数据推送线程
            startPushThread();
            notifyCallback(1, 0, "推流已启动");
            notifyCallback(1, 0, "推流线程已启动,等待推流状态就绪");
        } catch (Exception e) {
            Timber.e(e, "Failed to start push");
            notifyCallback(1, -3, "启动推流失败: " + e.getMessage());
        }
    }
    /**
     * 停止推流
     */
    public void stopPush() {
        Timber.d("stopPush called");
        stopPushThread();
//        stopAudioTransfer();
        stopWaterMaskSchedule();
        releaseAlivcPusher();
        if (usbCamera != null) {
            usbCamera.stopCamera();
        }
        pushStarted = false;
        notifyCallback(1, 4, "推流已停止");
    }
    /**
     * 初始化阿里推流
     */
    private void initAlivcPusher() {
        try {
            alivcLivePushConfig = new AlivcLivePushConfig();
            // 根据分辨率设置
            setResolutionFromArray(resolutionArr);
            // 建议用户使用20fps
            alivcLivePushConfig.setFps(AlivcFpsEnum.FPS_20);
            // 打开码率自适应
            alivcLivePushConfig.setEnableBitrateControl(true);
            // 设置横屏方向
            alivcLivePushConfig.setPreviewOrientation(AlivcPreviewOrientationEnum.ORIENTATION_LANDSCAPE_HOME_LEFT);
            // 设置音频编码模式
            alivcLivePushConfig.setAudioProfile(AlivcAudioAACProfileEnum.AAC_LC);
            // 设置摄像头类型
            alivcLivePushConfig.setCameraType(AlivcLivePushCameraTypeEnum.CAMERA_TYPE_BACK);
            // 设置视频编码模式为硬编码
            alivcLivePushConfig.setVideoEncodeMode(AlivcEncodeModeEnum.Encode_MODE_HARD);
            // 关闭美颜
            alivcLivePushConfig.setBeautyOn(false);
            // 清晰度优先模式
            alivcLivePushConfig.setQualityMode(AlivcQualityModeEnum.QM_RESOLUTION_FIRST);
            // 设置自定义流模式
            alivcLivePushConfig.setExternMainStream(true);
            alivcLivePushConfig.setAlivcExternMainImageFormat(AlivcImageFormat.IMAGE_FORMAT_YUV420P);
            // 初始化推流器
            alivcPusher = new AlivcLivePusher();
            alivcPusher.init(context.getApplicationContext(), alivcLivePushConfig);
            // 外部自定义流模式下,同样需要先开启预览,让状态从 INIT 进入 PREVIEWED
            // 创建一个隐藏的 Window 来承载 SurfaceView,确保 Surface 能够被创建
            windowManager = (WindowManager) context.getApplicationContext().getSystemService(Context.WINDOW_SERVICE);
            previewSurfaceView = new SurfaceView(context.getApplicationContext());
            // 在 SurfaceView 的 surfaceCreated 回调中再启动预览,确保 Surface 已经创建
            previewSurfaceView.getHolder().addCallback(new SurfaceHolder.Callback() {
                @Override
                public void surfaceCreated(SurfaceHolder holder) {
                    try {
                        Timber.d("previewSurfaceView surfaceCreated, startPreviewAysnc");
                        if (alivcPusher != null) {
                            alivcPusher.startPreviewAysnc(previewSurfaceView);
                            // 启动摄像头数据推送线程
                            startPushThread();
                        }
                    } catch (Exception e) {
                        Timber.e(e, "startPreviewAysnc in surfaceCreated failed");
                        notifyCallback(1, -3, "预览启动失败: " + e.getMessage());
                    }
                }
                @Override
                public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
                    Timber.d("previewSurfaceView surfaceChanged: %dx%d", width, height);
                }
                @Override
                public void surfaceDestroyed(SurfaceHolder holder) {
                    Timber.d("previewSurfaceView surfaceDestroyed");
                }
            });
            // 将 SurfaceView 添加到隐藏的 Window 中,这样 Surface 才会被创建
            WindowManager.LayoutParams params = new WindowManager.LayoutParams(
                    1, 1, // 1x1 像素,几乎不可见
                    WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
                    WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                            | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
                            | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
                            | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
                    android.graphics.PixelFormat.TRANSLUCENT
            );
            params.x = -1000; // 移到屏幕外
            params.y = -1000;
            params.alpha = 0.0f; // 完全透明
            try {
                windowManager.addView(previewSurfaceView, params);
                Timber.d("previewSurfaceView added to window");
            } catch (Exception e) {
                Timber.e(e, "Failed to add previewSurfaceView to window");
                // 如果添加失败,尝试使用 TYPE_APPLICATION 类型
                params.type = WindowManager.LayoutParams.TYPE_APPLICATION;
                try {
                    windowManager.addView(previewSurfaceView, params);
                    Timber.d("previewSurfaceView added to window with TYPE_APPLICATION");
                } catch (Exception e2) {
                    Timber.e(e2, "Failed to add previewSurfaceView with TYPE_APPLICATION");
                }
            }
            // 设置监听器
            setupListeners();
            Timber.d("AlivcPusher initialized successfully");
        } catch (Exception e) {
            Timber.e(e, "Failed to initialize AlivcPusher");
            notifyCallback(1, -3, "初始化推流SDK失败: " + e.getMessage());
        }
    }
    WatermarkParam watermarkParam;
    ArrayList<WatermarkParam> watermarkParamList = new ArrayList<>();
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    int baseY = 20;
    int fontSize= 24;
    private ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
    private void setWaterMask() {
        // 防止重复 schedule(startPush 可能被多次调用)
        if (watermarkFuture != null && !watermarkFuture.isCancelled()) {
            return;
        }
        if (scheduledExecutorService == null || scheduledExecutorService.isShutdown()) {
            scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
        }
        watermarkFuture = scheduledExecutorService.scheduleWithFixedDelay(() -> {
            if (pushStarted){
                if (!TextUtils.isEmpty(GlobalData.getInstance().getWaterMaskInfo())){
                    Log.i(TAG,"tieshuiin");
                    if (resolutionArr[0]==320&&resolutionArr[1]==240){
                        fontSize = 24;
                        baseY = 2;
                    }else if (resolutionArr[0]==640&&resolutionArr[1]==480){
                        fontSize = 32;
                        baseY = 4;
                    }else if (resolutionArr[0]==1280&&resolutionArr[1]==720){
                        fontSize = 48;
                        baseY = 6;
                    }else{
                        baseY = 2;
                        fontSize = 24;
                    }
                    String school = GlobalData.getInstance().parseWaterMaskInfo("school", "无", GlobalData.ShareType.STRING);
                    watermarkParam = new WatermarkParam(10,baseY,school);
                    watermarkParamList.clear();
                    watermarkParamList.add(watermarkParam);
                    String teacher = GlobalData.getInstance().parseWaterMaskInfo("teacher", "无", GlobalData.ShareType.STRING);
                    String stu = GlobalData.getInstance().parseWaterMaskInfo("student", "无", GlobalData.ShareType.STRING);
                    baseY = fontSize*11/10+baseY;
                    watermarkParam = new WatermarkParam(10,baseY,"教练:"+teacher+" 学员:"+stu);
                    watermarkParamList.add(watermarkParam);
                    double speed = GlobalData.getInstance().parseWaterMaskInfo("speed", 0.0, GlobalData.ShareType.DOUBLE);
                    String czh = GlobalData.getInstance().parseWaterMaskInfo("car_license", "无", GlobalData.ShareType.STRING) + GlobalData.getInstance().getCameraTag;
                    baseY = fontSize*11/10+baseY;
                    watermarkParam = new WatermarkParam(10,resolutionArr[1]-baseY,czh +"    "+String.format("速度:%.1f",speed));
                    watermarkParamList.add(watermarkParam);
                    double latitude = GlobalData.getInstance().parseWaterMaskInfo("latitude", 29.51228918, GlobalData.ShareType.DOUBLE);
                    double longitude = GlobalData.getInstance().parseWaterMaskInfo("longitude", 106.45556208, GlobalData.ShareType.DOUBLE);
//                            new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())
                    baseY = fontSize*11/10+baseY;
                    watermarkParam = new WatermarkParam(10,resolutionArr[1]-fontSize,  String.format("%.6f %.6f", latitude, longitude)+" "+sdf.format(new Date()));
                    watermarkParamList.add(watermarkParam);
                    if (resolutionArr[0]==320&&resolutionArr[1]==240){
                        usbCamera.enableWatermark(true,"/system/ms_unicode_24.bin");
                        usbCamera.setWatermark(3,fontSize,1,watermarkParamList);
                    }else if (resolutionArr[0]==640&&resolutionArr[1]==480){
                        usbCamera.enableWatermark(true,"/system/ms_unicode_32.bin");
                        usbCamera.setWatermark(3,fontSize,1,watermarkParamList);
                    }else if (resolutionArr[0]==1280&&resolutionArr[1]==720){
                        usbCamera.enableWatermark(true,"/system/ms_unicode_48.bin");
                        usbCamera.setWatermark(3,fontSize,1,watermarkParamList);
                    }else{
                        usbCamera.enableWatermark(true,"/system/ms_unicode_24.bin");
                        usbCamera.setWatermark(3,fontSize,1,watermarkParamList);
                    }
                }
            }
        },1,1, TimeUnit.SECONDS);
    }
    private ScheduledFuture<?> watermarkFuture;
    private void stopWaterMaskSchedule() {
        try {
            if (watermarkFuture != null) {
                watermarkFuture.cancel(true);
                watermarkFuture = null;
            }
        } catch (Throwable t) {
            Timber.w(t, "cancel watermarkFuture failed");
        }
        try {
            if (scheduledExecutorService != null && !scheduledExecutorService.isShutdown()) {
                scheduledExecutorService.shutdownNow();
            }
        } catch (Throwable t) {
            Timber.w(t, "shutdown watermark scheduledExecutorService failed");
        } finally {
            scheduledExecutorService = null;
        }
    }
    private Handler mainHandler = new Handler(Looper.getMainLooper());
    /**
     * 设置监听器
     */
@@ -191,77 +394,79 @@
            @Override
            public void onPreviewStarted(AlivcLivePusher alivcLivePusher) {
                Timber.d("onPreviewStarted");
                android.os.Handler handler = new android.os.Handler(android.os.Looper.getMainLooper());
                handler.postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        if (alivcPusher != null &&
                            alivcPusher.getCurrentStatus() != AlivcLivePushStats.PREVIEWED &&
                            alivcPusher.getCurrentStatus() != AlivcLivePushStats.PUSHED) {
                            Timber.w("Preview状态异常");
                        } else {
                            if (cameraExists && pushUrl != null && !pushUrl.isEmpty()) {
                                Timber.d("开始推流: %s", pushUrl);
                                alivcPusher.startPushAysnc(pushUrl);
                            }
                mainHandler.postDelayed(()->{
                    // 预览就绪后再启动推流,避免 INIT 状态直接 startPush 报错
                    if (alivcPusher != null && pushUrl != null && !pushUrl.isEmpty()) {
                        try {
                            AlivcLivePushStats s = alivcPusher.getCurrentStatus();
                            Timber.i("onPreviewStarted, current status=%s", s != null ? s.name() : "null");
                            Timber.d("开始推流: %s", pushUrl);
                            alivcPusher.startPushAysnc(pushUrl);
                        } catch (Exception e) {
                            Timber.e(e, "startPushAysnc failed");
                            notifyCallback(1, -3, "启动推流失败: " + e.getMessage());
                        }
                    }
                }, 1000);
                },1000);
            }
            @Override
            public void onPreviewStoped(AlivcLivePusher alivcLivePusher) {
                Timber.d("onPreviewStoped");
            }
            @Override
            public void onPushStarted(AlivcLivePusher alivcLivePusher) {
                Timber.d("onPushStarted");
                pushStarted = true;
//                startAudioTransfer();
                notifyCallback(1, 0, "推流已开始,分辨率: " + resolutionArr[0] + "x" + resolutionArr[1]);
            }
            @Override
            public void onPushPauesed(AlivcLivePusher alivcLivePusher) {
                Timber.d("onPushPauesed");
            }
            @Override
            public void onPushResumed(AlivcLivePusher alivcLivePusher) {
                Timber.d("onPushResumed");
            }
            @Override
            public void onPushStoped(AlivcLivePusher alivcLivePusher) {
                Timber.d("onPushStoped");
                pushStarted = false;
                notifyCallback(1, 4, "推流已停止");
            }
            @Override
            public void onPushRestarted(AlivcLivePusher alivcLivePusher) {
                Timber.d("onPushRestarted");
            }
            @Override
            public void onFirstFramePreviewed(AlivcLivePusher alivcLivePusher) {
                Timber.d("onFirstFramePreviewed");
            }
            @Override
            public void onDropFrame(AlivcLivePusher alivcLivePusher, int i, int i1) {
                // 丢帧回调
            }
            @Override
            public void onAdjustBitRate(AlivcLivePusher alivcLivePusher, int i, int i1) {
                // 码率调整回调
            }
            @Override
            public void onAdjustFps(AlivcLivePusher alivcLivePusher, int i, int i1) {
                // 帧率调整回调
            }
        });
        // 错误监听器
        alivcPusher.setLivePushErrorListener(new AlivcLivePushErrorListener() {
            @Override
@@ -272,7 +477,7 @@
                    alivcLivePusher.stopPush();
                }
            }
            @Override
            public void onSDKError(AlivcLivePusher alivcLivePusher, AlivcLivePushError alivcLivePushError) {
                Timber.e("onSDKError: %s", alivcLivePushError.toString());
@@ -282,7 +487,7 @@
                }
            }
        });
        // 网络监听器
        alivcPusher.setLivePushNetworkListener(new AlivcLivePushNetworkListener() {
            @Override
@@ -290,54 +495,54 @@
                Timber.w("onNetworkPoor");
                notifyCallback(1, 3, "网络较差");
            }
            @Override
            public void onNetworkRecovery(AlivcLivePusher alivcLivePusher) {
                Timber.d("onNetworkRecovery");
                notifyCallback(1, 0, "网络恢复");
            }
            @Override
            public void onReconnectStart(AlivcLivePusher alivcLivePusher) {
                Timber.d("onReconnectStart");
            }
            @Override
            public void onReconnectFail(AlivcLivePusher alivcLivePusher) {
                Timber.e("onReconnectFail");
                notifyCallback(1, 2, "重连失败");
            }
            @Override
            public void onReconnectSucceed(AlivcLivePusher alivcLivePusher) {
                Timber.d("onReconnectSucceed");
                notifyCallback(1, 0, "重连成功");
            }
            @Override
            public void onSendDataTimeout(AlivcLivePusher alivcLivePusher) {
                Timber.w("onSendDataTimeout");
            }
            @Override
            public void onConnectFail(AlivcLivePusher alivcLivePusher) {
                Timber.e("onConnectFail");
                notifyCallback(1, -2, "连接失败");
            }
            @Override
            public String onPushURLAuthenticationOverdue(AlivcLivePusher alivcLivePusher) {
                Timber.w("onPushURLAuthenticationOverdue");
                return null;
            }
            @Override
            public void onSendMessage(AlivcLivePusher alivcLivePusher) {
                // 发送消息回调
            }
        });
    }
    /**
     * 根据分辨率数组设置分辨率
     */
@@ -368,7 +573,7 @@
            }
        }
    }
    /**
     * 打开USB摄像头
     */
@@ -381,8 +586,17 @@
            // 打开摄像头之前先调用setenv
            usbCamera.setenv();
            // 使用prepareCamera方法,camera_id范围[0,9]
            int[] cameraIds = {0, 9};
            // 使用 prepareCamera 方法;根据 MediaArgu.usbCameraId 选择具体摄像头
            // usbCameraId: 1 -> P1(0), 2 -> P2(2), 其他 -> 让库自行在 {0,2} 里选择
            int usbId = (mediaArgu != null) ? mediaArgu.getUsbCameraId() : 0;
            int[] cameraIds;
            if (usbId == 2) {
                cameraIds = new int[]{2};
            } else if (usbId == 1) {
                cameraIds = new int[]{0};
            } else {
                cameraIds = new int[]{0, 2};
            }
            String cameraName = null; // 不指定特定名称
            // 如果返回非0,代表打开失败,则先stopCamera再重试,最多3次
@@ -404,7 +618,7 @@
            return false;
        }
    }
    /**
     * 启动推流线程
     */
@@ -416,7 +630,7 @@
            Timber.d("Push thread started");
        }
    }
    /**
     * 停止推流线程
     */
@@ -432,17 +646,30 @@
        }
        Timber.d("Push thread stopped");
    }
    /**
     * 释放阿里推流资源
     */
    private void releaseAlivcPusher() {
        // 兜底:防止外部没有走 stopPush
        stopWaterMaskSchedule();
        // 移除隐藏的 SurfaceView
        if (previewSurfaceView != null && windowManager != null) {
            try {
                windowManager.removeView(previewSurfaceView);
                Timber.d("previewSurfaceView removed from window");
            } catch (Exception e) {
                Timber.e(e, "Error removing previewSurfaceView from window");
            }
            previewSurfaceView = null;
        }
        if (alivcPusher != null) {
            try {
                AlivcLivePushStats stats = alivcPusher.getCurrentStatus();
                Timber.d("当前推流状态: %s", stats != null ? stats.name() : "null");
                if (stats != null && (stats == AlivcLivePushStats.PUSHED ||
                if (stats != null && (stats == AlivcLivePushStats.PUSHED ||
                    stats == AlivcLivePushStats.PREVIEWED)) {
                    alivcPusher.stopPush();
                }
@@ -453,8 +680,9 @@
            alivcPusher = null;
        }
        alivcLivePushConfig = null;
        pushStarted = false;
    }
    /**
     * 推流线程
     */
@@ -463,17 +691,18 @@
        public void run() {
            super.run();
            Timber.d("PushThread started");
            try {
                int width = resolutionArr[0];
                int height = resolutionArr[1];
                final long startTimeNs = System.nanoTime();
                // 计算YUV420缓冲区大小
                int bufferSize = width * height * 3 / 2;
                byte[] buffer = new byte[bufferSize];
                Timber.d("开始推送视频数据,分辨率: %dx%d", width, height);
                // 循环处理摄像头数据
                while (isRunning && cameraExists) {
                    // 处理摄像头数据
@@ -489,19 +718,23 @@
                    usbCamera.rgba(1, buffer);
                    
                    // 推流数据到阿里云
                    if (alivcPusher != null && cameraExists) {
                    if (alivcPusher != null && cameraExists && pushStarted) {
                        try {
                            long ptsUs = (System.nanoTime() - startTimeNs) / 1000;
                            alivcPusher.inputStreamVideoData(
                                    buffer,
                                    width,
                                    height,
                                    buffer.length,
                                    System.nanoTime() / 1000, // 转换为微秒
                                    ptsUs, // 单调递增的时间戳(微秒)
                                    0 // rotation
                            );
                        } catch (Exception e) {
                            Timber.e(e, "Error pushing frame");
                        }
                    } else if (!pushStarted) {
                        // 等待 onPushStarted 后再喂帧,避免 SDK invalid state
                        Thread.sleep(20);
                    }
                    
                    // 控制帧率,约20fps
@@ -530,4 +763,52 @@
            callback.onResult(response);
        }
    }
    private void startAudioTransfer() {
        Timber.i("开始通过mic录制声音,上传");
        // 创建单线程线程池用于音频推流
        if (audioPushExecutor == null || audioPushExecutor.isShutdown()) {
            audioPushExecutor = Executors.newSingleThreadExecutor(r -> {
                Thread thread = new Thread(r, "AudioPushThread");
                thread.setDaemon(true);
                return thread;
            });
        }
        AudioRecordManager.getInstance().startRecording((data, size) -> {
            if (alivcPusher != null && audioPushExecutor != null && !audioPushExecutor.isShutdown()) {
                // 在单线程线程池中执行音频推流
                audioPushExecutor.execute(() -> {
                    try {
                        alivcPusher.inputStreamAudioData(data, data.length, System.nanoTime() / 1000);
                    } catch (Exception e) {
                        Timber.e(e, "Error pushing audio data");
                    }
                });
            }
        });
    }
    private void stopAudioTransfer() {
        Timber.i("停止通过mic录制声音,上传");
        AudioRecordManager.getInstance().stopRecording();
        // 停止并关闭音频推流线程池
        if (audioPushExecutor != null && !audioPushExecutor.isShutdown()) {
            audioPushExecutor.shutdown();
            try {
                // 等待最多1秒让任务完成
                if (!audioPushExecutor.awaitTermination(1, java.util.concurrent.TimeUnit.SECONDS)) {
                    audioPushExecutor.shutdownNow();
                }
            } catch (InterruptedException e) {
                audioPushExecutor.shutdownNow();
                Thread.currentThread().interrupt();
            }
            audioPushExecutor = null;
            Timber.d("音频推流线程池已关闭");
        }
    }
}
app/src/main/java/com/safeluck/floatwindow/manager/UsbCameraRecordManager.java
@@ -8,10 +8,14 @@
import android.media.MediaFormat;
import android.media.MediaMuxer;
import android.media.MediaRecorder;
import android.text.TextUtils;
import android.util.Log;
import com.anyun.libusbcamera.UsbCamera;
import com.anyun.libusbcamera.WatermarkParam;
import com.safeluck.floatwindow.MediaArgu;
import com.safeluck.floatwindow.ResponseVO;
import com.safeluck.floatwindow.util.GlobalData;
import com.safeluck.floatwindow.util.VideoFileUtils;
import timber.log.Timber;
@@ -19,6 +23,13 @@
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
/**
 * USB摄像头录像管理器
@@ -116,7 +127,7 @@
                notifyCallback(0, -1, "USB摄像头打开失败");
                return;
            }
            setWaterMask();
            cameraExists = true;
            Timber.d("USB摄像头打开成功");
            
@@ -129,12 +140,122 @@
            notifyCallback(0, -3, "启动录像失败: " + e.getMessage());
        }
    }
    WatermarkParam watermarkParam;
    ArrayList<WatermarkParam> watermarkParamList = new ArrayList<>();
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    int baseY = 20;
    int fontSize= 24;
    private ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
    private ScheduledFuture<?> watermarkFuture;
    private void setWaterMask() {
        // 防止重复 schedule(startRecord 可能被多次调用)
        if (watermarkFuture != null && !watermarkFuture.isCancelled()) {
            return;
        }
        if (scheduledExecutorService == null || scheduledExecutorService.isShutdown()) {
            scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
        }
        watermarkFuture = scheduledExecutorService.scheduleWithFixedDelay(() -> {
                if (!TextUtils.isEmpty(GlobalData.getInstance().getWaterMaskInfo())){
                    Log.i(TAG,"tieshuiin");
                    if (resolutionArr[0]==320&&resolutionArr[1]==240){
                        fontSize = 24;
                        baseY = 2;
                    }else if (resolutionArr[0]==640&&resolutionArr[1]==480){
                        fontSize = 32;
                        baseY = 4;
                    }else if (resolutionArr[0]==1280&&resolutionArr[1]==720){
                        fontSize = 48;
                        baseY = 6;
                    }else{
                        baseY = 2;
                        fontSize = 24;
                    }
                    String school = GlobalData.getInstance().parseWaterMaskInfo("school", "无", GlobalData.ShareType.STRING);
                    watermarkParam = new WatermarkParam(10,baseY,school);
                    watermarkParamList.clear();
                    watermarkParamList.add(watermarkParam);
                    String teacher = GlobalData.getInstance().parseWaterMaskInfo("teacher", "无", GlobalData.ShareType.STRING);
                    String stu = GlobalData.getInstance().parseWaterMaskInfo("student", "无", GlobalData.ShareType.STRING);
                    baseY = fontSize*11/10+baseY;
                    watermarkParam = new WatermarkParam(10,baseY,"教练:"+teacher+" 学员:"+stu);
                    watermarkParamList.add(watermarkParam);
                    double speed = GlobalData.getInstance().parseWaterMaskInfo("speed", 0.0, GlobalData.ShareType.DOUBLE);
                    String czh = GlobalData.getInstance().parseWaterMaskInfo("car_license", "无", GlobalData.ShareType.STRING) + GlobalData.getInstance().getCameraTag;
                    baseY = fontSize*11/10+baseY;
                    watermarkParam = new WatermarkParam(10,resolutionArr[1]-baseY,czh +"    "+String.format("速度:%.1f",speed));
                    watermarkParamList.add(watermarkParam);
                    double latitude = GlobalData.getInstance().parseWaterMaskInfo("latitude", 29.51228918, GlobalData.ShareType.DOUBLE);
                    double longitude = GlobalData.getInstance().parseWaterMaskInfo("longitude", 106.45556208, GlobalData.ShareType.DOUBLE);
//                            new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())
                    baseY = fontSize*11/10+baseY;
                    watermarkParam = new WatermarkParam(10,resolutionArr[1]-fontSize,  String.format("%.6f %.6f", latitude, longitude)+" "+sdf.format(new Date()));
                    watermarkParamList.add(watermarkParam);
                    if (resolutionArr[0]==320&&resolutionArr[1]==240){
                        usbCamera.enableWatermark(true,"/system/ms_unicode_24.bin");
                        usbCamera.setWatermark(3,fontSize,1,watermarkParamList);
                    }else if (resolutionArr[0]==640&&resolutionArr[1]==480){
                        usbCamera.enableWatermark(true,"/system/ms_unicode_32.bin");
                        usbCamera.setWatermark(3,fontSize,1,watermarkParamList);
                    }else if (resolutionArr[0]==1280&&resolutionArr[1]==720){
                        usbCamera.enableWatermark(true,"/system/ms_unicode_48.bin");
                        usbCamera.setWatermark(3,fontSize,1,watermarkParamList);
                    }else{
                        usbCamera.enableWatermark(true,"/system/ms_unicode_24.bin");
                        usbCamera.setWatermark(3,fontSize,1,watermarkParamList);
                    }
                }
        },1,1, TimeUnit.SECONDS);
    }
    private void stopWaterMaskSchedule() {
        Timber.i("%s_stopWaterMaskSchedule", TAG);
        try {
            if (watermarkFuture != null) {
                watermarkFuture.cancel(true);
                watermarkFuture = null;
            }
        } catch (Throwable t) {
            Timber.w(t, "cancel watermarkFuture failed");
        }
        try {
            if (scheduledExecutorService != null && !scheduledExecutorService.isShutdown()) {
                scheduledExecutorService.shutdownNow();
            }
        } catch (Throwable t) {
            Timber.w(t, "shutdown watermark scheduledExecutorService failed");
        } finally {
            scheduledExecutorService = null;
        }
    }
    /**
     * 停止录像
     */
    public void stopRecord() {
        Timber.d("stopRecord called");
        stopWaterMaskSchedule();
        
        // 停止音频线程
        if (audioThread != null) {
@@ -167,8 +288,17 @@
            // 打开摄像头之前先调用setenv
            usbCamera.setenv();
            // 使用prepareCamera方法,camera_id范围[0,9]
            int[] cameraIds = {0, 2};
            // 使用 prepareCamera 方法;根据 MediaArgu.usbCameraId 选择具体摄像头
            // usbCameraId: 1 -> P1(0), 2 -> P2(2), 其他 -> 让库自行在 {0,2} 里选择
            int usbId = (mediaArgu != null) ? mediaArgu.getUsbCameraId() : 0;
            int[] cameraIds;
            if (usbId == 2) {
                cameraIds = new int[]{2};
            } else if (usbId == 1) {
                cameraIds = new int[]{0};
            } else {
                cameraIds = new int[]{0, 2};
            }
            String cameraName = null; // 不指定特定名称
            // 如果返回非0,代表打开失败,则先stopCamera再重试,最多3次
@@ -263,6 +393,7 @@
     * 释放资源
     */
    private void releaseResources() {
        if (audioRecord != null) {
            try {
                if (audioRecord.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING) {
app/src/main/java/com/safeluck/floatwindow/util/AudioRecordManager.java
New file
@@ -0,0 +1,123 @@
package com.safeluck.floatwindow.util;
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.MediaRecorder;
import android.os.Environment;
import android.util.Log;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
/**
 * aaa
 * Created by lzw on 2019/8/29. 11:19:57
 * 邮箱:632393724@qq.com
 * All Rights Saved! Chongqing AnYun Tech co. LTD
 */
public class AudioRecordManager {
    private static final String TAG = "AudioRecordManager";
    private static AudioRecordManager instance;
    /***标记是否正在录音**/
    private boolean isRecording = false;
    private AudioRecord audioRecord;
    /***最小缓冲区大小*/
    private int bufferSize = 0;
    /***采样率*/
    private int sampleRateInHz = 32000;
    /***量化位数**/
    private int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
    /***存放音频数据的buffer**/
    private byte[] buffer;
    private int channelConf = AudioFormat.CHANNEL_IN_STEREO;
    private OnAudioRecordListener onAudioRecordListener;
    private AudioRecordManager() {
        //计算最小缓冲区
        bufferSize = AudioRecord.getMinBufferSize(sampleRateInHz, channelConf, audioFormat);
//        bufferSize = bufferSize > 320 ? 320 : bufferSize;
        Log.i(TAG,"mini buffersize="+bufferSize);
        audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, sampleRateInHz, channelConf, audioFormat, bufferSize);
    }
    public static AudioRecordManager getInstance() {
        if (instance == null) {
            synchronized (AudioRecordManager.class) {
                if (instance == null) {
                    instance = new AudioRecordManager();
                }
            }
        }
        return instance;
    }
    /***
     * 开始采集音频
     */
    public void startRecording(final OnAudioRecordListener onAudioRecordListener) {
        setAudioRecordListener(onAudioRecordListener);
        if (audioRecord == null){
            audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, sampleRateInHz, channelConf, audioFormat, bufferSize);
        }
        buffer = new byte[bufferSize];
                isRecording = true;
                audioRecord.startRecording();
                try {
                    while (isRecording){
                        int readSize = audioRecord.read(buffer,0,bufferSize);
                        Log.i(TAG, "run: buffer length"+buffer.length+" readSize="+readSize);
                        if (onAudioRecordListener != null){
                            onAudioRecordListener.onVoiceRecord(buffer,bufferSize);
                        }
                    }
                    audioRecord.stop();
                    destroy();
                } catch (Exception e) {
                    e.printStackTrace();
                }
    }
    /***
     * 停止音频采集
     */
    public void stopRecording(){
        Log.i(TAG, "stopRecording");
        isRecording = false;
        setAudioRecordListener(null);
    }
    public void destroy(){
        if (audioRecord!=null){
            audioRecord.release();
            audioRecord = null;
        }
    }
    public interface OnAudioRecordListener {
        /***
         * 采集到的音频信息回调到上层
         * @param data
         * @param size
         */
        void onVoiceRecord(byte[] data,int size);
    }
    private void setAudioRecordListener(OnAudioRecordListener onAudioRecordListener){
        this.onAudioRecordListener = onAudioRecordListener;
    }
}
app/src/main/java/com/safeluck/floatwindow/util/GlobalData.java
New file
@@ -0,0 +1,128 @@
package com.safeluck.floatwindow.util;
import android.text.TextUtils;
import android.util.Log;
import com.safeluck.floatwindow.MediaArgu;
import org.json.JSONException;
import org.json.JSONObject;
/**
 * @ProjectName: aaa
 * @Package: com.safeluck.floatwindow.bean
 * @ClassName: GlobalData
 * @Description: 存放全局数据,到处都可以调用;  单例
 * @Author: zhanwei.li
 * @CreateDate: 2021/8/7 10:13
 * @UpdateUser: 更新者
 * @UpdateDate: 2021/8/7 10:13
 * @UpdateRemark: 更新说明
 * @Version: 1.0
 */
public class GlobalData {
    private static final String TAG = "GlobalData";
    public static final String CPU_MODEL_8953 = "MSM8953";
    public String getCameraTag = "_P1";
    //水印信息
    /*
    {"teacher": "张三",
            "student": "李四",
            "speed": 24.5,
            "longitude": "26.3231,
            "latitude": 109.3233,
            "school": "测试驾校",
            "car_license": "渝A1234学",
            "other": "",
    }
     */
    private String waterMaskInfo;
    private GlobalData() {
    }
    private static GlobalData instance;
    public static GlobalData getInstance() {
        if (instance == null) {
            synchronized (GlobalData.class) {
                if (instance == null) {
                    instance = new GlobalData();
                }
            }
        }
        return instance;
    }
    public void setWaterMaskInfo(String info){
        Log.i(TAG,"waterMaskInfo="+info);
        this.waterMaskInfo = info;
    }
    public String getWaterMaskInfo(){
        if (TextUtils.isEmpty(waterMaskInfo)){
            waterMaskInfo = "     {\"teacher\": \"\",\n" +
                    "            \"student\": "+stuName+",\n" +
                    "            \"speed\": 0,\n" +
                    "            \"longitude\": \"00.0000,\n" +
                    "            \"latitude\": 00.0000,\n" +
                    "            \"school\": \"\",\n" +
                    "            \"car_license\": \"\",\n" +
                    "            \"other\": \"\",\n" +
                    "    }";
        }
        return waterMaskInfo;
    }
    public<T> T parseWaterMaskInfo(String key,T defaultValue,ShareType type){
        Object value = null;
        Log.i(TAG,"parseWaterMaskInfo="+key);
        if (!TextUtils.isEmpty(waterMaskInfo)){
            try {
                JSONObject jsonObject = new JSONObject(waterMaskInfo);
                switch (type){
                    case STRING:
                        value = jsonObject.getString(key);
                        break;
                    case INTEGER:
                        value = jsonObject.getInt(key);
                        break;
                    case LONG:
                        value = jsonObject.getLong(key);
                        break;
                    case DOUBLE:
                        value = jsonObject.getDouble(key);
                        break;
                    case BOOLEAN:
                        value = jsonObject.getBoolean(key);
                        break;
                }
                Log.i(TAG,"key="+key+" value="+value);
                return (T)value;
            } catch (JSONException e) {
                e.printStackTrace();
            }
        }
        return defaultValue;
    }
    private MediaArgu mediaArgu;
    public MediaArgu getMediaArgu() {
        return mediaArgu;
    }
    public void setMediaArgu(MediaArgu mediaArgu){
        this.mediaArgu = mediaArgu;
    }
    private String stuName="";
    public void setStuName(String userName) {
        stuName = userName;
    }
    public enum ShareType{
        STRING,INTEGER,LONG,DOUBLE,BOOLEAN
    }
}
readMe.md
New file
@@ -0,0 +1,375 @@
* MediaMuxer 启动后不能再添加轨道
* 视频和音频的时间戳需要正确同步
现在代码应该:
* 正确等待两个轨道都准备好
* 同步启动 MediaMuxer
* 正确写入视频和音频数据
* 避免状态冲突导致的错误
mp4文件明显时间加快了,现实世界1秒钟,可能mp4文件的视频就是半秒,所有录一分钟的视频mp4文件,视频只播到30秒就不再播了
检查并修复时间戳计算问题,可能是帧率或时间戳计算不正确:
修复时间戳计算:改用实际时间而非帧索引,确保时间戳准确:
修复时间戳计算,使用实际时间而非帧索引:
UsbCameraRecordManager.java
long frameCount = 0;
// 记录开始时间(纳秒,用于精确时间戳)
long recordingStartTimeNs = System.nanoTime();
long frameCount = 0;
UsbCameraRecordManager.java
if (videoEncoder != null && mediaMuxer != null) {
encodeFrame(buffer, frameCount, width, height);
frameCount++;
encodeFrame(buffer, frameCount, width, height);
if (videoEncoder != null && mediaMuxer != null) {
// 计算实际经过的时间(微秒)
long elapsedTimeUs = (System.nanoTime() - recordingStartTimeNs) / 1000;
encodeFrame(buffer, elapsedTimeUs, width, height);
frameCount++;
UsbCameraRecordManager.java
* 编码一帧数据
*/
* 编码一帧数据
* @param yuvData YUV数据
* @param presentationTimeUs 时间戳(微秒),基于实际开始时间
* @param width 宽度
* @param height 高度
*/
修复音频时间戳,使其与视频时间戳同步:
UsbCameraRecordManager.java
+3
/**
// 录制开始时间(纳秒),用于时间戳同步
private volatile long recordingStartTimeNs = 0;
/**
UsbCameraRecordManager.java
+1-1
// 记录开始时间(纳秒,用于精确时间戳)
long recordingStartTimeNs = System.nanoTime();
long frameCount = 0;
long recordingStartTimeNs = System.nanoTime();
// 记录开始时间(纳秒,用于精确时间戳)
recordingStartTimeNs = System.nanoTime();
long frameCount = 0;
UsbCameraRecordManager.java
+3-3
byte[] audioBuffer = new byte[audioBufferSize];
long audioFrameCount = 0;
// 编码音频数据
long audioFrameCount = 0;
byte[] audioBuffer = new byte[audioBufferSize];
long totalSamplesRead = 0; // 总采样数
// 编码音频数据
UsbCameraRecordManager.java
+7-2
* 编码音频数据
*/
private void encodeAudio(byte[] audioData, int size, long frameCount) {
private void encodeAudio(byte[] audioData, int size, long frameCount) {
* 编码音频数据
* @param audioData 音频数据
* @param size 数据大小(字节)
* @param totalSamples 总采样数(从开始到现在的累计采样数)
*/
在创建新文件时重置开始时间:
UsbCameraRecordManager.java
+3
// 重新启动音频录制
// 重置开始时间
recordingStartTimeNs = System.nanoTime();
// 重新启动音频录制
已修复时间戳计算问题。主要修改如下:修复内容:
* * *
1. 视频时间戳使用实际时间:
* 之前:使用 (frameIndex * 1000000) / FRAME_RATE 计算,假设固定帧率
* 现在:使用 (System.nanoTime() - recordingStartTimeNs) / 1000,基于实际经过时间
1. 音频时间戳基于采样数:
* 使用 (totalSamples * 1000000) / SAMPLE_RATE 计算
* totalSamples 是累计采样数,确保时间戳连续
1. 添加了录制开始时间记录:
* 使用 recordingStartTimeNs 记录开始时间(纳秒)
* 在创建新文件时重置,确保每个文件的时间戳从 0 开始
1. 确保时间戳同步:
* 视频和音频都从同一个基准时间开始
* 每个新文件的时间戳都从 0 开始
修复原理:
* 问题原因:使用帧索引计算时间戳,假设固定帧率。实际采集帧率可能不同,导致时间戳不准确
* 解决方案:使用实际经过的时间计算时间戳,确保时间戳与实际时间一致
现在:
* 视频时间戳基于实际经过的时间
* 音频时间戳基于实际读取的采样数
* 两者都从 0 开始,确保同步
#### 使用
          private var mediaAidlInterface: IMediaAidlInterface? = null
        private var serviceConnection: ServiceConnection? = null
        private var isServiceBound = false
        private val isServiceBoundState = mutableStateOf(false)
            serviceConnection = object : ServiceConnection {
                override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
                    mediaAidlInterface = IMediaAidlInterface.Stub.asInterface(service)
                    isServiceBound = true
                    isServiceBoundState.value = true
                    Timber.d("FloatingService connected")
                }
                override fun onServiceDisconnected(name: ComponentName?) {
                    mediaAidlInterface = null
                    isServiceBound = false
                    isServiceBoundState.value = false
                    Timber.d("FloatingService disconnected")
           onBindService = {
                                if (!isServiceBoundState.value && serviceConnection != null) {
                                    val intent = Intent(this@MainActivity, FloatingService::class.java)
                                    bindService(intent, serviceConnection!!, Context.BIND_AUTO_CREATE)
                                    Timber.d("Binding FloatingService")
                                }
                            }, }
            }
        private fun startAndroidRecord() {
            if (mediaAidlInterface == null) {
                Timber.w("Service not bound, cannot start Android record")
                return
            }
            try {
                val mediaArgu = MediaArgu().apply {
                    isPush = false
                    isUsedOutCamera = false // Android 内置摄像头
                    codeRate = 0
                    frameRate = 0
                    m_screen = MediaArgu.ScreenSolution(640, 480) // 默认分辨率
                    recordTime = 0
                    tfCardFlag = 0 // 内部存储
                }
                mediaAidlInterface?.registerCallback(callback)
                mediaAidlInterface?.startMedia(mediaArgu)
                Timber.d("Started Android camera record")
            } catch (e: RemoteException) {
                Timber.e(e, "Error starting Android record")
            }
        }
        private fun stopAndroidRecord() {
            if (mediaAidlInterface == null) {
                Timber.w("Service not bound, cannot stop Android record")
                return
            }
            try {
                mediaAidlInterface?.stopMedia()
                Timber.d("Stopped Android camera record")
            } catch (e: RemoteException) {
                Timber.e(e, "Error stopping Android record")
            }
        }
        private fun startUsbRecord() {
            if (mediaAidlInterface == null) {
                Timber.w("Service not bound, cannot start USB record")
                return
            }
            try {
                val mediaArgu = MediaArgu().apply {
                    isPush = false
                    isUsedOutCamera = true // USB 摄像头
                    codeRate = 0
                    frameRate = 0
                    m_screen = MediaArgu.ScreenSolution(640, 480) // 默认分辨率
                    recordTime = 0
                    tfCardFlag = 0 // 内部存储
                }
                mediaAidlInterface?.registerCallback(callback)
                mediaAidlInterface?.startMedia(mediaArgu)
                Timber.d("Started USB camera record")
            } catch (e: RemoteException) {
                Timber.e(e, "Error starting USB record")
            }
        }
        private fun stopUsbRecord() {
            if (mediaAidlInterface == null) {
                Timber.w("Service not bound, cannot stop USB record")
                return
            }
            try {
                mediaAidlInterface?.stopMedia()
                Timber.d("Stopped USB camera record")
            } catch (e: RemoteException) {
                Timber.e(e, "Error stopping USB record")
            }
        }
        private fun startUsbPush() {
            if (mediaAidlInterface == null) {
                Timber.w("Service not bound, cannot start USB push")
                return
            }
            try {
                val mediaArgu = MediaArgu().apply {
                    isPush = true
                    isUsedOutCamera = true // USB 摄像头
                    codeRate = 0
                    frameRate = 0
                    m_screen = MediaArgu.ScreenSolution(640, 480) // 默认分辨率
                    url = "rtmp://192.168.16.143/live/livestream" // TODO: 需要设置实际的推流地址
                    userName = ""
                    pwd = ""
                }
                mediaAidlInterface?.registerCallback(callback)
                mediaAidlInterface?.startMedia(mediaArgu)
                Timber.d("Started USB camera push")
            } catch (e: RemoteException) {
                Timber.e(e, "Error starting USB push")
            }
        }
        private fun stopUsbPush() {
            if (mediaAidlInterface == null) {
                Timber.w("Service not bound, cannot stop USB push")
                return
            }
            try {
                mediaAidlInterface?.stopMedia()
                Timber.d("Stopped USB camera push")
            } catch (e: RemoteException) {
                Timber.e(e, "Error stopping USB push")
            }
        }
        private fun unbindServiceInternal() {
            if (isServiceBound && serviceConnection != null) {
                try {
                    mediaAidlInterface?.unregisterCallback(callback)
                } catch (e: RemoteException) {
                    Timber.e(e, "Error unregistering callback")
                }
                try {
                    unbindService(serviceConnection!!)
                    Timber.d("Unbinding FloatingService")
                } catch (e: Exception) {
                    Timber.e(e, "Error unbinding service")
                }
                isServiceBound = false
                isServiceBoundState.value = false
                mediaAidlInterface = null
            }
        }
usbcameralib/src/main/cpp/watermark.cpp
@@ -66,7 +66,7 @@
    pic_width = width;
    pic_height = height;
    AppTimer_add(PrintTime, D_SEC(1));
//    AppTimer_add(PrintTime, D_SEC(1));
}
void UninitWatermark(void)