app/build.gradle.kts
@@ -1,3 +1,6 @@ import java.io.FileInputStream import java.util.Properties plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) @@ -7,7 +10,9 @@ android { namespace = "com.safeluck.floatwindow" compileSdk = 35 val keystorePropertiesFile = rootProject.file("keystore.properties") val keystoreProperties = Properties() keystoreProperties.load(FileInputStream(keystorePropertiesFile)) defaultConfig { applicationId = "com.safeluck.floatwindow" minSdk = 24 @@ -16,9 +21,27 @@ versionName = "1.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" ndk { abiFilters += listOf("arm64-v8a", "armeabi-v7a") } } buildFeatures{ aidl =true } signingConfigs { create("release") { keyPassword = keystoreProperties.getProperty("keyPassword") keyAlias = keystoreProperties.getProperty("keyAlias") storeFile = file(keystoreProperties.getProperty("storeFile") ?: "") storePassword = keystoreProperties.getProperty("storePassword") } getByName("debug") { keyPassword = keystoreProperties.getProperty("keyPassword") keyAlias = keystoreProperties.getProperty("keyAlias") storeFile = file(keystoreProperties.getProperty("storeFile") ?: "") storePassword = keystoreProperties.getProperty("storePassword") } } buildTypes { release { @@ -27,6 +50,10 @@ getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) signingConfig = signingConfigs.getByName("release") } debug { signingConfig = signingConfigs.getByName("debug") } } compileOptions { @@ -39,6 +66,15 @@ buildFeatures { compose = true } packaging { jniLibs { useLegacyPackaging = false } resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" } } } dependencies { app/build/generated/aidl_source_output_dir/debug/out/com/safeluck/floatwindow/IMediaAidlInterface.java
@@ -1,6 +1,6 @@ /* * This file is auto-generated. DO NOT MODIFY. * Using: D:\Program\ Files\Android\Sdk\build-tools\35.0.0\aidl.exe -pD:\Program\ Files\Android\Sdk\platforms\android-35\framework.aidl -oD:\JetBrainsProjects\AndroidProject\anyunVideo\app\build\generated\aidl_source_output_dir\debug\out -ID:\JetBrainsProjects\AndroidProject\anyunVideo\app\src\main\aidl -ID:\JetBrainsProjects\AndroidProject\anyunVideo\app\src\debug\aidl -ID:\data\.gradle\caches\8.10.2\transforms\53a750d70626c759bd7a6dbaf50185ee\transformed\core-1.12.0\aidl -ID:\data\.gradle\caches\8.10.2\transforms\dc945394860d4e1c7d02ff0c8d3e2e6f\transformed\versionedparcelable-1.1.1\aidl -dC:\Users\Dana\AppData\Local\Temp\aidl14282392422309074909.d D:\JetBrainsProjects\AndroidProject\anyunVideo\app\src\main\aidl\com\safeluck\floatwindow\IMediaAidlInterface.aidl * Using: D:\Program\ Files\Android\Sdk\build-tools\35.0.0\aidl.exe -pD:\Program\ Files\Android\Sdk\platforms\android-35\framework.aidl -oD:\JetBrainsProjects\AndroidProject\anyunVideo\app\build\generated\aidl_source_output_dir\debug\out -ID:\JetBrainsProjects\AndroidProject\anyunVideo\app\src\main\aidl -ID:\JetBrainsProjects\AndroidProject\anyunVideo\app\src\debug\aidl -ID:\data\.gradle\caches\8.10.2\transforms\53a750d70626c759bd7a6dbaf50185ee\transformed\core-1.12.0\aidl -ID:\data\.gradle\caches\8.10.2\transforms\dc945394860d4e1c7d02ff0c8d3e2e6f\transformed\versionedparcelable-1.1.1\aidl -dC:\Users\Dana\AppData\Local\Temp\aidl10441886692587460682.d D:\JetBrainsProjects\AndroidProject\anyunVideo\app\src\main\aidl\com\safeluck\floatwindow\IMediaAidlInterface.aidl */ package com.safeluck.floatwindow; // Declare any non-default types here with import statements app/build/generated/aidl_source_output_dir/debug/out/com/safeluck/floatwindow/IMyCallback.java
@@ -1,6 +1,6 @@ /* * This file is auto-generated. DO NOT MODIFY. * Using: D:\Program\ Files\Android\Sdk\build-tools\35.0.0\aidl.exe -pD:\Program\ Files\Android\Sdk\platforms\android-35\framework.aidl -oD:\JetBrainsProjects\AndroidProject\anyunVideo\app\build\generated\aidl_source_output_dir\debug\out -ID:\JetBrainsProjects\AndroidProject\anyunVideo\app\src\main\aidl -ID:\JetBrainsProjects\AndroidProject\anyunVideo\app\src\debug\aidl -ID:\data\.gradle\caches\8.10.2\transforms\53a750d70626c759bd7a6dbaf50185ee\transformed\core-1.12.0\aidl -ID:\data\.gradle\caches\8.10.2\transforms\dc945394860d4e1c7d02ff0c8d3e2e6f\transformed\versionedparcelable-1.1.1\aidl -dC:\Users\Dana\AppData\Local\Temp\aidl6375257616930721574.d D:\JetBrainsProjects\AndroidProject\anyunVideo\app\src\main\aidl\com\safeluck\floatwindow\IMyCallback.aidl * Using: D:\Program\ Files\Android\Sdk\build-tools\35.0.0\aidl.exe -pD:\Program\ Files\Android\Sdk\platforms\android-35\framework.aidl -oD:\JetBrainsProjects\AndroidProject\anyunVideo\app\build\generated\aidl_source_output_dir\debug\out -ID:\JetBrainsProjects\AndroidProject\anyunVideo\app\src\main\aidl -ID:\JetBrainsProjects\AndroidProject\anyunVideo\app\src\debug\aidl -ID:\data\.gradle\caches\8.10.2\transforms\53a750d70626c759bd7a6dbaf50185ee\transformed\core-1.12.0\aidl -ID:\data\.gradle\caches\8.10.2\transforms\dc945394860d4e1c7d02ff0c8d3e2e6f\transformed\versionedparcelable-1.1.1\aidl -dC:\Users\Dana\AppData\Local\Temp\aidl6260459078460591487.d D:\JetBrainsProjects\AndroidProject\anyunVideo\app\src\main\aidl\com\safeluck\floatwindow\IMyCallback.aidl */ package com.safeluck.floatwindow; /** app/build/intermediates/desugar_graph/debug/dexBuilderDebug/out/currentProject/dirs_bucket_9/graph.binBinary files differ
app/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tabBinary files differ
app/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.values.atBinary files differ
app/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tabBinary files differ
app/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.values.atBinary files differ
app/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/package-parts.tabBinary files differ
app/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/package-parts.tab.values.atBinary files differ
app/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tabBinary files differ
app/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab.values.atBinary files differ
app/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tabBinary files differ
app/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab.values.atBinary files differ
app/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/counters.tab
@@ -1,2 +1,2 @@ 4 5 0 app/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tabBinary files differ
app/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.values.atBinary files differ
app/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tabBinary files differ
app/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.keystreamBinary files differ
app/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.keystream.lenBinary files differ
app/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.lenBinary files differ
app/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.values.atBinary files differ
app/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab_iBinary files differ
app/src/main/AndroidManifest.xml
@@ -1,10 +1,12 @@ <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> android:sharedUserId="android.uid.system" xmlns:tools="http://schemas.android.com/tools" > <uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <application android:allowBackup="true" android:name=".H264Application" app/src/main/java/com/safeluck/floatwindow/MainActivity.kt
@@ -1,47 +1,363 @@ package com.safeluck.floatwindow import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.ServiceConnection import android.os.Bundle import android.os.IBinder import android.os.RemoteException import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.* import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.safeluck.floatwindow.ui.theme.AnyunVideoTheme import timber.log.Timber class MainActivity : ComponentActivity() { private var mediaAidlInterface: IMediaAidlInterface? = null private var serviceConnection: ServiceConnection? = null private var isServiceBound = false private val isServiceBoundState = mutableStateOf(false) private val callback = object : IMyCallback.Stub() { @Throws(RemoteException::class) override fun onResult(re: ResponseVO?) { re?.let { Timber.d("Callback received: type=${it.type}, errCode=${it.errCode}, message=${it.message}") } } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() 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") } } setContent { AnyunVideoTheme { Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> Greeting( name = "Android", modifier = Modifier.padding(innerPadding) Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background ) { MainScreen( isServiceBound = isServiceBoundState.value, 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") } }, onUnbindService = { unbindServiceInternal() }, onStartAndroidRecord = { startAndroidRecord() }, onStopAndroidRecord = { stopAndroidRecord() }, onStartUsbRecord = { startUsbRecord() }, onStopUsbRecord = { stopUsbRecord() }, onStartUsbPush = { startUsbPush() }, onStopUsbPush = { stopUsbPush() } ) } } } } } @Composable fun Greeting(name: String, modifier: Modifier = Modifier) { Text( text = "Hello $name!", modifier = modifier ) } @Preview(showBackground = true) @Composable fun GreetingPreview() { AnyunVideoTheme { Greeting("Android") 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://your-push-url" // 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 } } override fun onDestroy() { super.onDestroy() unbindServiceInternal() } } @Composable fun MainScreen( isServiceBound: Boolean, onBindService: () -> Unit, onUnbindService: () -> Unit, onStartAndroidRecord: () -> Unit, onStopAndroidRecord: () -> Unit, onStartUsbRecord: () -> Unit, onStopUsbRecord: () -> Unit, onStartUsbPush: () -> Unit, onStopUsbPush: () -> Unit ) { Column( modifier = Modifier.fillMaxSize() ) { // 固定顶部:标题和状态 Column( modifier = Modifier .fillMaxWidth() .padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally ) { Text( text = "FloatingService 控制", style = MaterialTheme.typography.headlineMedium, modifier = Modifier.padding(bottom = 8.dp) ) Text( text = if (isServiceBound) "服务状态: 已绑定" else "服务状态: 未绑定", style = MaterialTheme.typography.bodyMedium, color = if (isServiceBound) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error, modifier = Modifier.padding(bottom = 16.dp) ) } Divider() // 可滚动的按钮列表 Column( modifier = Modifier .fillMaxSize() .verticalScroll(rememberScrollState()) .padding(horizontal = 16.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(12.dp) ) { Spacer(modifier = Modifier.height(12.dp)) // 按钮 1: 绑定服务 Button( onClick = onBindService, modifier = Modifier.fillMaxWidth(), enabled = !isServiceBound ) { Text("1. 绑定 FloatingService") } // 按钮 2: 解绑服务 Button( onClick = onUnbindService, modifier = Modifier.fillMaxWidth(), enabled = isServiceBound ) { Text("2. 解绑 FloatingService") } Divider(modifier = Modifier.padding(vertical = 8.dp)) // 按钮 3: 开始 Android 相机录像 Button( onClick = onStartAndroidRecord, modifier = Modifier.fillMaxWidth(), enabled = isServiceBound ) { Text("3. 开始 Android 相机录像") } // 按钮 4: 结束 Android 相机录像 Button( onClick = onStopAndroidRecord, modifier = Modifier.fillMaxWidth(), enabled = isServiceBound ) { Text("4. 结束 Android 相机录像") } Divider(modifier = Modifier.padding(vertical = 8.dp)) // 按钮 5: 开始 USB 相机录像 Button( onClick = onStartUsbRecord, modifier = Modifier.fillMaxWidth(), enabled = isServiceBound ) { Text("5. 开始 USB 相机录像") } // 按钮 6: 结束 USB 相机录像 Button( onClick = onStopUsbRecord, modifier = Modifier.fillMaxWidth(), enabled = isServiceBound ) { Text("6. 结束 USB 相机录像") } Divider(modifier = Modifier.padding(vertical = 8.dp)) // 按钮 7: 开始 USB 推流 Button( onClick = onStartUsbPush, modifier = Modifier.fillMaxWidth(), enabled = isServiceBound ) { Text("7. 开始 USB 推流") } // 按钮 8: 结束 USB 推流 Button( onClick = onStopUsbPush, modifier = Modifier.fillMaxWidth(), enabled = isServiceBound ) { Text("8. 结束 USB 推流") } Spacer(modifier = Modifier.height(12.dp)) } } } app/src/main/java/com/safeluck/floatwindow/MediaArgu.java
@@ -107,6 +107,7 @@ this.pwd = pwd; } public static class ScreenSolution implements Parcelable { int width; int height; app/src/main/java/com/safeluck/floatwindow/manager/AndroidCameraRecordManager.java
@@ -186,15 +186,31 @@ } // 设置MediaRecorder // 注意:MediaRecorder的设置顺序非常重要,必须严格按照以下顺序: mediaRecorder = new MediaRecorder(); // 1. 设置数据源(必须在setOutputFormat之前) mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); mediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE); // 2. 设置输出格式(必须在setOutputFile和编码器之前) mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); // 3. 设置输出文件(必须在编码器之前) mediaRecorder.setOutputFile(currentVideoFile.getAbsolutePath()); // 4. 设置编码器(必须在setOutputFormat之后) mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC); mediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264); // 5. 设置编码参数(必须在编码器之后) mediaRecorder.setAudioEncodingBitRate(64000); // 64kbps mediaRecorder.setAudioSamplingRate(44100); // 44.1kHz mediaRecorder.setVideoEncodingBitRate(width * height * 3); mediaRecorder.setVideoFrameRate(20); mediaRecorder.setVideoSize(width, height); mediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264); // 6. 准备MediaRecorder mediaRecorder.prepare(); // 创建Surface app/src/main/java/com/safeluck/floatwindow/manager/UsbCameraRecordManager.java
@@ -70,6 +70,9 @@ private File currentVideoFile; private long currentFileStartTime; // 录制开始时间(纳秒),用于时间戳同步 private volatile long recordingStartTimeNs = 0; /** * 录像回调接口 */ @@ -165,7 +168,7 @@ usbCamera.setenv(); // 使用prepareCamera方法,camera_id范围[0,9] int[] cameraIds = {0, 9}; int[] cameraIds = {0, 2}; String cameraName = null; // 不指定特定名称 // 如果返回非0,代表打开失败,则先stopCamera再重试,最多3次 @@ -366,12 +369,18 @@ Timber.d("音频录制已启动"); } // 注意:不要在这里主动检查音频编码器输出格式 // 因为 MediaCodec 的 getOutputFormat() 在编码器启动后可能返回 null // 应该等待 INFO_OUTPUT_FORMAT_CHANGED 事件 // 启动音频编码线程 audioThread = new AudioThread(); audioThread.start(); Timber.d("开始录像,分辨率: %dx%d", width, height); // 记录开始时间(纳秒,用于精确时间戳) recordingStartTimeNs = System.nanoTime(); long frameCount = 0; long lastFileChangeTime = System.currentTimeMillis(); @@ -400,6 +409,9 @@ break; } // 重置开始时间 recordingStartTimeNs = System.nanoTime(); // 重新启动音频录制 if (audioRecord != null) { audioRecord.startRecording(); @@ -426,11 +438,13 @@ } // 获取YUV数据 (参数0表示录像) usbCamera.rgba(0, buffer); usbCamera.rgba(2, buffer); // 编码并写入文件 if (videoEncoder != null && mediaMuxer != null) { encodeFrame(buffer, frameCount, width, height); // 计算实际经过的时间(微秒) long elapsedTimeUs = (System.nanoTime() - recordingStartTimeNs) / 1000; encodeFrame(buffer, elapsedTimeUs, width, height); frameCount++; } @@ -461,8 +475,12 @@ /** * 编码一帧数据 * @param yuvData YUV数据 * @param presentationTimeUs 时间戳(微秒),基于实际开始时间 * @param width 宽度 * @param height 高度 */ private void encodeFrame(byte[] yuvData, long frameIndex, int width, int height) { private void encodeFrame(byte[] yuvData, long presentationTimeUs, int width, int height) { try { // 获取输入缓冲区 int inputBufferIndex = videoEncoder.dequeueInputBuffer(10000); @@ -472,7 +490,7 @@ inputBuffer.clear(); inputBuffer.put(yuvData); long presentationTimeUs = (frameIndex * 1000000) / FRAME_RATE; // 使用实际经过的时间作为时间戳 videoEncoder.queueInputBuffer(inputBufferIndex, 0, yuvData.length, presentationTimeUs, 0); } @@ -482,18 +500,35 @@ MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); int outputBufferIndex = videoEncoder.dequeueOutputBuffer(bufferInfo, 0); while (outputBufferIndex >= 0) { while (outputBufferIndex != MediaCodec.INFO_TRY_AGAIN_LATER) { if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { // 输出格式改变,添加视频轨道 MediaFormat newFormat = videoEncoder.getOutputFormat(); videoTrackIndex = mediaMuxer.addTrack(newFormat); checkAndStartMuxer(); synchronized (UsbCameraRecordManager.this) { // 检查 muxer 是否已启动,如果已启动则无法添加轨道 if (muxerStarted) { Timber.e("Muxer already started, cannot add video track"); break; } if (videoTrackIndex < 0) { videoTrackIndex = mediaMuxer.addTrack(newFormat); Timber.d("视频轨道已添加: %d", videoTrackIndex); checkAndStartMuxer(); } } outputBufferIndex = videoEncoder.dequeueOutputBuffer(bufferInfo, 0); continue; } else if (outputBufferIndex >= 0) { ByteBuffer outputBuffer = videoEncoder.getOutputBuffer(outputBufferIndex); // 只有在 muxer 已启动且轨道索引有效时才写入数据 if (outputBuffer != null && muxerStarted && videoTrackIndex >= 0) { outputBuffer.position(bufferInfo.offset); outputBuffer.limit(bufferInfo.offset + bufferInfo.size); mediaMuxer.writeSampleData(videoTrackIndex, outputBuffer, bufferInfo); try { mediaMuxer.writeSampleData(videoTrackIndex, outputBuffer, bufferInfo); } catch (Exception e) { Timber.e(e, "Error writing video sample data"); } } videoEncoder.releaseOutputBuffer(outputBufferIndex, false); } @@ -512,9 +547,15 @@ */ private synchronized void checkAndStartMuxer() { if (!muxerStarted && videoTrackIndex >= 0 && audioTrackIndex >= 0) { mediaMuxer.start(); muxerStarted = true; try { mediaMuxer.start(); muxerStarted = true; Timber.d("Muxer started, video track: %d, audio track: %d", videoTrackIndex, audioTrackIndex); } catch (Exception e) { Timber.e(e, "Failed to start muxer"); } } else { Timber.d("Muxer not started yet, video track: %d, audio track: %d", videoTrackIndex, audioTrackIndex); } } @@ -535,7 +576,7 @@ try { byte[] audioBuffer = new byte[audioBufferSize]; long audioFrameCount = 0; long totalSamplesRead = 0; // 总采样数 while (isRecording && audioRecord != null && audioEncoder != null) { // 读取音频数据 @@ -546,8 +587,8 @@ } // 编码音频数据 encodeAudio(audioBuffer, readSize, audioFrameCount); audioFrameCount += readSize / 2; // 16位采样,每个采样2字节 encodeAudio(audioBuffer, readSize, totalSamplesRead); totalSamplesRead += readSize / 2; // 16位采样,每个采样2字节 } } catch (Exception e) { @@ -559,10 +600,13 @@ /** * 编码音频数据 * @param audioData 音频数据 * @param size 数据大小(字节) * @param totalSamples 总采样数(从开始到现在的累计采样数) */ private void encodeAudio(byte[] audioData, int size, long frameCount) { private void encodeAudio(byte[] audioData, int size, long totalSamples) { try { // 获取输入缓冲区 // 获取输入缓冲区(即使 muxer 未启动也要编码,以便尽快获得输出格式) int inputBufferIndex = audioEncoder.dequeueInputBuffer(10000); if (inputBufferIndex >= 0) { ByteBuffer inputBuffer = audioEncoder.getInputBuffer(inputBufferIndex); @@ -570,7 +614,9 @@ inputBuffer.clear(); inputBuffer.put(audioData, 0, size); long presentationTimeUs = (frameCount * 1000000) / SAMPLE_RATE; // 使用采样数计算时间戳(微秒) // totalSamples 是采样数,SAMPLE_RATE 是每秒采样数 long presentationTimeUs = (totalSamples * 1000000) / SAMPLE_RATE; audioEncoder.queueInputBuffer(inputBufferIndex, 0, size, presentationTimeUs, 0); } @@ -580,18 +626,35 @@ MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); int outputBufferIndex = audioEncoder.dequeueOutputBuffer(bufferInfo, 0); while (outputBufferIndex >= 0) { while (outputBufferIndex != MediaCodec.INFO_TRY_AGAIN_LATER) { if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { // 输出格式改变,添加音频轨道 MediaFormat newFormat = audioEncoder.getOutputFormat(); audioTrackIndex = mediaMuxer.addTrack(newFormat); checkAndStartMuxer(); synchronized (UsbCameraRecordManager.this) { // 检查 muxer 是否已启动,如果已启动则无法添加轨道 if (muxerStarted) { Timber.e("Muxer already started, cannot add audio track"); break; } if (audioTrackIndex < 0) { audioTrackIndex = mediaMuxer.addTrack(newFormat); Timber.d("音频轨道已添加: %d", audioTrackIndex); checkAndStartMuxer(); } } outputBufferIndex = audioEncoder.dequeueOutputBuffer(bufferInfo, 0); continue; } else if (outputBufferIndex >= 0) { ByteBuffer outputBuffer = audioEncoder.getOutputBuffer(outputBufferIndex); // 只有在 muxer 已启动且轨道索引有效时才写入数据 if (outputBuffer != null && muxerStarted && audioTrackIndex >= 0) { outputBuffer.position(bufferInfo.offset); outputBuffer.limit(bufferInfo.offset + bufferInfo.size); mediaMuxer.writeSampleData(audioTrackIndex, outputBuffer, bufferInfo); try { mediaMuxer.writeSampleData(audioTrackIndex, outputBuffer, bufferInfo); } catch (Exception e) { Timber.e(e, "Error writing audio sample data"); } } audioEncoder.releaseOutputBuffer(outputBufferIndex, false); } app/src/main/java/com/safeluck/floatwindow/util/VideoFileUtils.java
@@ -1,6 +1,7 @@ package com.safeluck.floatwindow.util; import android.content.Context; import android.os.Environment; import timber.log.Timber; @@ -36,7 +37,7 @@ baseDir = new File(storagePath); } else { // 内部存储 baseDir = context.getFilesDir(); baseDir = Environment.getExternalStorageDirectory(); } // 创建 AnYun_VIDEO 目录 key/Verify.txt
New file @@ -0,0 +1,41 @@ private static PrivateKey getKey(String key, char[] password) throws Exception { byte[] cabuf = new BASE64Decoder().decodeBuffer(key); KeyStore keyStore = KeyStore.getInstance("PKCS12"); keyStore.load(new ByteArrayInputStream(cabuf), password); Enumeration<String> aliases = keyStore.aliases(); if (!aliases.hasMoreElements()) { throw new RuntimeException("no alias found"); } String alias = aliases.nextElement(); PrivateKey privateKey = (RSAPrivateCrtKeyImpl) keyStore.getKey(alias, password); return privateKey; } public static String sign(String data, PrivateKey key) throws Exception { // MessageDigest md = MessageDigest.getInstance("SHA-256"); MessageDigest md = MessageDigest.getInstance("SHA-1"); md.update(data.getBytes("utf-8")); byte[] hash = md.digest(); // Cipher cipher = Cipher.getInstance("RSA"); Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); cipher.init(Cipher.ENCRYPT_MODE, key); byte[] encrypted = cipher.doFinal(hash); return HexBin.encode(encrypted); } public static String sign(String data, String keyString ,String pwd ) throws Exception { char[] password = pwd.toCharArray(); return sign(data,getKey(keyString,password)); } public static void main(String[] args) { try{ String sign = sign("xuanborobot123456nullnullnull","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","27U777"); System.out.println(sign); }catch (Exception e){ e.printStackTrace(); } } key/bjkey.jksBinary files differ
key/key.jksBinary files differ
key/key.jks.oriBinary files differ
key/keyrk3288.jksBinary files differ
key/keysc200.jksBinary files differ
key/keysc626.jksBinary files differ
keystore.properties
New file @@ -0,0 +1,4 @@ storePassword = 123456 keyPassword = 123456 keyAlias = key0 storeFile = ../key/key.jks usbcameralib/build.gradle
@@ -16,6 +16,8 @@ cmake { arguments "-DANDROID_ARM_NEON=TRUE" cppFlags "" // 确保使用 c++_shared STL arguments "-DANDROID_STL=c++_shared" } } ndk { @@ -43,7 +45,9 @@ pickFirst 'lib/armeabi-v7a/libturbojpeg.so' pickFirst 'lib/arm64-v8a/libjpeg.so' pickFirst 'lib/arm64-v8a/libturbojpeg.so' // 确保 libc++_shared.so 被包含 pickFirst 'lib/armeabi-v7a/libc++_shared.so' pickFirst 'lib/arm64-v8a/libc++_shared.so' } }