xuning

录音实现

@@ -3,6 +3,8 @@ @@ -3,6 +3,8 @@
3 android:versionCode="1" 3 android:versionCode="1"
4 android:versionName="1.1"> 4 android:versionName="1.1">
5 <uses-permission android:name="android.permission.CAMERA" /> 5 <uses-permission android:name="android.permission.CAMERA" />
  6 + <uses-permission android:name="android.permission.RECORD_AUDIO" />
  7 + <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
6 <uses-feature android:name="android.hardware.camera2.full" /> 8 <uses-feature android:name="android.hardware.camera2.full" />
7 <uses-feature android:name="android.hardware.camera" android:required="false" /> 9 <uses-feature android:name="android.hardware.camera" android:required="false" />
8 10
@@ -19,6 +19,7 @@ import android.app.Activity; @@ -19,6 +19,7 @@ import android.app.Activity;
19 import android.content.pm.PackageManager; 19 import android.content.pm.PackageManager;
20 import android.graphics.PixelFormat; 20 import android.graphics.PixelFormat;
21 import android.os.Bundle; 21 import android.os.Bundle;
  22 +import android.os.Environment;
22 import android.util.Log; 23 import android.util.Log;
23 import android.view.Surface; 24 import android.view.Surface;
24 import android.view.SurfaceHolder; 25 import android.view.SurfaceHolder;
@@ -28,6 +29,7 @@ import android.view.WindowManager; @@ -28,6 +29,7 @@ import android.view.WindowManager;
28 import android.widget.AdapterView; 29 import android.widget.AdapterView;
29 import android.widget.Button; 30 import android.widget.Button;
30 import android.widget.Spinner; 31 import android.widget.Spinner;
  32 +import android.widget.Toast;
31 33
32 import android.support.v4.app.ActivityCompat; 34 import android.support.v4.app.ActivityCompat;
33 import android.support.v4.content.ContextCompat; 35 import android.support.v4.content.ContextCompat;
@@ -35,9 +37,11 @@ import android.support.v4.content.ContextCompat; @@ -35,9 +37,11 @@ import android.support.v4.content.ContextCompat;
35 public class MainActivity extends Activity implements SurfaceHolder.Callback 37 public class MainActivity extends Activity implements SurfaceHolder.Callback
36 { 38 {
37 public static final int REQUEST_CAMERA = 100; 39 public static final int REQUEST_CAMERA = 100;
  40 + public static final int REQUEST_RECORD_AUDIO = 101;
38 41
39 private YOLO11Ncnn yolo11ncnn = new YOLO11Ncnn(); 42 private YOLO11Ncnn yolo11ncnn = new YOLO11Ncnn();
40 private int facing = 0; 43 private int facing = 0;
  44 + private boolean isRecording = false;
41 45
42 private Spinner spinnerTask; 46 private Spinner spinnerTask;
43 private Spinner spinnerModel; 47 private Spinner spinnerModel;
@@ -77,6 +81,36 @@ public class MainActivity extends Activity implements SurfaceHolder.Callback @@ -77,6 +81,36 @@ public class MainActivity extends Activity implements SurfaceHolder.Callback
77 } 81 }
78 }); 82 });
79 83
  84 + final Button buttonRecord = (Button) findViewById(R.id.buttonRecord);
  85 + buttonRecord.setOnClickListener(new View.OnClickListener() {
  86 + @Override
  87 + public void onClick(View arg0) {
  88 + if (!isRecording) {
  89 + // 检查录音权限
  90 + if (ContextCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_DENIED) {
  91 + ActivityCompat.requestPermissions(MainActivity.this, new String[] {Manifest.permission.RECORD_AUDIO}, REQUEST_RECORD_AUDIO);
  92 + return;
  93 + }
  94 +
  95 + // 生成文件名
  96 + String timestamp = String.valueOf(System.currentTimeMillis());
  97 + String filepath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES).getAbsolutePath() + "/yolo11_record_" + timestamp + ".mp4";
  98 +
  99 + if (yolo11ncnn.startRecording(filepath)) {
  100 + isRecording = true;
  101 + buttonRecord.setText("停止录制");
  102 + Toast.makeText(MainActivity.this, "开始录制", Toast.LENGTH_SHORT).show();
  103 + }
  104 + } else {
  105 + if (yolo11ncnn.stopRecording()) {
  106 + isRecording = false;
  107 + buttonRecord.setText("开始录制");
  108 + Toast.makeText(MainActivity.this, "录制已停止", Toast.LENGTH_SHORT).show();
  109 + }
  110 + }
  111 + }
  112 + });
  113 +
80 spinnerTask = (Spinner) findViewById(R.id.spinnerTask); 114 spinnerTask = (Spinner) findViewById(R.id.spinnerTask);
81 spinnerTask.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { 115 spinnerTask.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
82 @Override 116 @Override
@@ -23,6 +23,9 @@ public class YOLO11Ncnn @@ -23,6 +23,9 @@ public class YOLO11Ncnn
23 public native boolean openCamera(int facing); 23 public native boolean openCamera(int facing);
24 public native boolean closeCamera(); 24 public native boolean closeCamera();
25 public native boolean setOutputWindow(Surface surface); 25 public native boolean setOutputWindow(Surface surface);
  26 + public native boolean startRecording(String filepath);
  27 + public native boolean stopRecording();
  28 + public native boolean isRecording();
26 29
27 static { 30 static {
28 System.loadLibrary("yolo11ncnn"); 31 System.loadLibrary("yolo11ncnn");
@@ -10,4 +10,4 @@ find_package(ncnn REQUIRED) @@ -10,4 +10,4 @@ find_package(ncnn REQUIRED)
10 10
11 add_library(yolo11ncnn SHARED yolo11ncnn.cpp yolo11.cpp yolo11_det.cpp yolo11_seg.cpp yolo11_pose.cpp yolo11_cls.cpp yolo11_obb.cpp ndkcamera.cpp) 11 add_library(yolo11ncnn SHARED yolo11ncnn.cpp yolo11.cpp yolo11_det.cpp yolo11_seg.cpp yolo11_pose.cpp yolo11_cls.cpp yolo11_obb.cpp ndkcamera.cpp)
12 12
13 -target_link_libraries(yolo11ncnn ncnn ${OpenCV_LIBS} camera2ndk mediandk) 13 +target_link_libraries(yolo11ncnn ncnn ${OpenCV_LIBS} camera2ndk mediandk)
@@ -15,10 +15,20 @@ @@ -15,10 +15,20 @@
15 #include "ndkcamera.h" 15 #include "ndkcamera.h"
16 16
17 #include <string> 17 #include <string>
  18 +#include <thread>
  19 +#include <queue>
  20 +#include <mutex>
18 21
19 #include <android/log.h> 22 #include <android/log.h>
  23 +#include <media/NdkMediaCodec.h>
  24 +#include <media/NdkMediaMuxer.h>
  25 +#include <media/NdkMediaFormat.h>
  26 +#include <chrono>
  27 +#include <unistd.h>
  28 +#include <fcntl.h>
20 29
21 #include <opencv2/core/core.hpp> 30 #include <opencv2/core/core.hpp>
  31 +#include <sys/time.h>
22 32
23 #include "mat.h" 33 #include "mat.h"
24 34
@@ -434,10 +444,25 @@ NdkCameraWindow::NdkCameraWindow() : NdkCamera() @@ -434,10 +444,25 @@ NdkCameraWindow::NdkCameraWindow() : NdkCamera()
434 sensor_manager = ASensorManager_getInstance(); 444 sensor_manager = ASensorManager_getInstance();
435 445
436 accelerometer_sensor = ASensorManager_getDefaultSensor(sensor_manager, ASENSOR_TYPE_ACCELEROMETER); 446 accelerometer_sensor = ASensorManager_getDefaultSensor(sensor_manager, ASENSOR_TYPE_ACCELEROMETER);
  447 +
  448 + // recording
  449 + recording_active = false;
  450 + recording_thread = nullptr;
  451 + video_encoder = nullptr;
  452 + audio_encoder = nullptr;
  453 + media_muxer = nullptr;
  454 + video_track_index = -1;
  455 + audio_track_index = -1;
  456 + muxer_started = false;
437 } 457 }
438 458
439 NdkCameraWindow::~NdkCameraWindow() 459 NdkCameraWindow::~NdkCameraWindow()
440 { 460 {
  461 + // stop recording if active
  462 + if (recording_active) {
  463 + stopRecording();
  464 + }
  465 +
441 if (accelerometer_sensor) 466 if (accelerometer_sensor)
442 { 467 {
443 ASensorEventQueue_disableSensor(sensor_event_queue, accelerometer_sensor); 468 ASensorEventQueue_disableSensor(sensor_event_queue, accelerometer_sensor);
@@ -769,3 +794,422 @@ void NdkCameraWindow::on_image(const unsigned char* nv21, int nv21_width, int nv @@ -769,3 +794,422 @@ void NdkCameraWindow::on_image(const unsigned char* nv21, int nv21_width, int nv
769 794
770 ANativeWindow_unlockAndPost(win); 795 ANativeWindow_unlockAndPost(win);
771 } 796 }
  797 +
  798 +// Recording functions implementation
  799 +bool NdkCameraWindow::startRecording(const char* filepath) {
  800 + std::lock_guard<std::mutex> lock(recording_mutex);
  801 +
  802 + if (recording_active) {
  803 + __android_log_print(ANDROID_LOG_ERROR, "NdkCamera", "Recording already active");
  804 + return false;
  805 + }
  806 +
  807 + // 保存文件路径
  808 + recording_filepath = filepath;
  809 +
  810 + // 创建MediaMuxer - 使用文件描述符
  811 + int fd = ::open(recording_filepath.c_str(), O_CREAT | O_RDWR | O_TRUNC, 0644);
  812 + if (fd < 0) {
  813 + __android_log_print(ANDROID_LOG_ERROR, "NdkCamera", "Failed to open file: %s", recording_filepath.c_str());
  814 + return false;
  815 + }
  816 +
  817 + media_muxer = AMediaMuxer_new(fd, AMEDIAMUXER_OUTPUT_FORMAT_MPEG_4);
  818 + if (!media_muxer) {
  819 + __android_log_print(ANDROID_LOG_ERROR, "NdkCamera", "Failed to create media muxer");
  820 + ::close(fd);
  821 + return false;
  822 + }
  823 +
  824 + // 初始化编码器
  825 + if (!setup_video_encoder(640, 480, 30)) {
  826 + __android_log_print(ANDROID_LOG_ERROR, "NdkCamera", "Failed to setup video encoder");
  827 + AMediaMuxer_delete(media_muxer);
  828 + media_muxer = nullptr;
  829 + ::close(fd);
  830 + return false;
  831 + }
  832 +
  833 + if (!setup_audio_encoder()) {
  834 + __android_log_print(ANDROID_LOG_ERROR, "NdkCamera", "Failed to setup audio encoder");
  835 + AMediaCodec_delete(video_encoder);
  836 + video_encoder = nullptr;
  837 + AMediaMuxer_delete(media_muxer);
  838 + media_muxer = nullptr;
  839 + ::close(fd);
  840 + return false;
  841 + }
  842 +
  843 + recording_active = true;
  844 + video_track_index = -1;
  845 + audio_track_index = -1;
  846 + muxer_started = false;
  847 +
  848 + // 清空帧队列
  849 + while (!video_frame_queue.empty()) {
  850 + video_frame_queue.pop();
  851 + }
  852 +
  853 + // 创建录制线程
  854 + recording_thread = new std::thread(&NdkCameraWindow::recording_worker, this);
  855 +
  856 + __android_log_print(ANDROID_LOG_INFO, "NdkCamera", "Recording started: %s", filepath);
  857 + return true;
  858 +}
  859 +
  860 +bool NdkCameraWindow::stopRecording() {
  861 + {
  862 + std::lock_guard<std::mutex> lock(recording_mutex);
  863 + if (!recording_active) {
  864 + __android_log_print(ANDROID_LOG_WARN, "NdkCamera", "Recording not active");
  865 + return false;
  866 + }
  867 + recording_active = false;
  868 + __android_log_print(ANDROID_LOG_INFO, "NdkCamera", "Recording flag set to false");
  869 + }
  870 +
  871 + // 等待录制线程结束
  872 + if (recording_thread && recording_thread->joinable()) {
  873 + __android_log_print(ANDROID_LOG_INFO, "NdkCamera", "Waiting for recording thread to finish...");
  874 + recording_thread->join();
  875 + delete recording_thread;
  876 + recording_thread = nullptr;
  877 + __android_log_print(ANDROID_LOG_INFO, "NdkCamera", "Recording thread finished");
  878 + }
  879 +
  880 + // 清理资源
  881 + if (media_muxer) {
  882 + __android_log_print(ANDROID_LOG_INFO, "NdkCamera", "Stopping media muxer");
  883 + if (muxer_started) {
  884 + AMediaMuxer_stop(media_muxer);
  885 + }
  886 + AMediaMuxer_delete(media_muxer);
  887 + media_muxer = nullptr;
  888 + }
  889 +
  890 + if (video_encoder) {
  891 + __android_log_print(ANDROID_LOG_INFO, "NdkCamera", "Stopping video encoder");
  892 + AMediaCodec_stop(video_encoder);
  893 + AMediaCodec_delete(video_encoder);
  894 + video_encoder = nullptr;
  895 + }
  896 +
  897 + if (audio_encoder) {
  898 + __android_log_print(ANDROID_LOG_INFO, "NdkCamera", "Stopping audio encoder");
  899 + AMediaCodec_stop(audio_encoder);
  900 + AMediaCodec_delete(audio_encoder);
  901 + audio_encoder = nullptr;
  902 + }
  903 +
  904 + // 清空帧队列
  905 + {
  906 + std::lock_guard<std::mutex> lock(recording_mutex);
  907 + while (!video_frame_queue.empty()) {
  908 + video_frame_queue.pop();
  909 + }
  910 + }
  911 +
  912 + __android_log_print(ANDROID_LOG_INFO, "NdkCamera", "Recording stopped successfully");
  913 + return true;
  914 +}
  915 +
  916 +bool NdkCameraWindow::isRecording() const {
  917 + std::lock_guard<std::mutex> lock(const_cast<std::mutex&>(recording_mutex));
  918 + return recording_active;
  919 +}
  920 +
  921 +void NdkCameraWindow::recording_worker() {
  922 + __android_log_print(ANDROID_LOG_INFO, "NdkCamera", "Recording worker started");
  923 +
  924 + int64_t start_time = get_current_timestamp();
  925 + int frame_count = 0;
  926 +
  927 + // 录制工作线程
  928 + while (true) {
  929 + {
  930 + std::lock_guard<std::mutex> lock(recording_mutex);
  931 + if (!recording_active) {
  932 + break;
  933 + }
  934 + }
  935 +
  936 + // 处理视频帧队列
  937 + std::vector<uint8_t> frame;
  938 + bool has_frame = false;
  939 + {
  940 + std::lock_guard<std::mutex> lock(recording_mutex);
  941 + if (!video_frame_queue.empty()) {
  942 + frame = video_frame_queue.front();
  943 + video_frame_queue.pop();
  944 + has_frame = true;
  945 + frame_count++;
  946 + }
  947 + }
  948 +
  949 + if (has_frame && video_encoder) {
  950 + // 编码视频帧
  951 + encode_video_frame_data(frame.data(), frame.size());
  952 + }
  953 +
  954 + // 处理编码器输出
  955 + process_encoder_output();
  956 +
  957 + // 记录录制时长
  958 + int64_t current_time = get_current_timestamp();
  959 + int64_t elapsed_seconds = (current_time - start_time) / 1000000;
  960 +
  961 + // 每5秒记录一次状态
  962 + if (elapsed_seconds > 0 && elapsed_seconds % 5 == 0) {
  963 + __android_log_print(ANDROID_LOG_INFO, "NdkCamera",
  964 + "Recording: %ld seconds, %d frames processed",
  965 + elapsed_seconds, frame_count);
  966 + }
  967 +
  968 + // 短暂休眠避免过度占用CPU
  969 + usleep(10000); // 10ms
  970 + }
  971 +
  972 + __android_log_print(ANDROID_LOG_INFO, "NdkCamera", "Recording worker stopping");
  973 +
  974 + // 录制结束时,处理剩余的编码器输出
  975 + if (video_encoder) {
  976 + process_encoder_output_final();
  977 + }
  978 +
  979 + __android_log_print(ANDROID_LOG_INFO, "NdkCamera", "Recording worker finished, total frames: %d", frame_count);
  980 +}
  981 +
  982 +bool NdkCameraWindow::setup_video_encoder(int width, int height, int fps) {
  983 + // 创建视频编码器
  984 + const char* mime_type = "video/avc";
  985 + video_encoder = AMediaCodec_createEncoderByType(mime_type);
  986 + if (!video_encoder) {
  987 + __android_log_print(ANDROID_LOG_ERROR, "NdkCamera", "Failed to create video encoder");
  988 + return false;
  989 + }
  990 +
  991 + // 配置视频格式
  992 + AMediaFormat* format = AMediaFormat_new();
  993 + AMediaFormat_setString(format, AMEDIAFORMAT_KEY_MIME, mime_type);
  994 + AMediaFormat_setInt32(format, AMEDIAFORMAT_KEY_WIDTH, width);
  995 + AMediaFormat_setInt32(format, AMEDIAFORMAT_KEY_HEIGHT, height);
  996 + AMediaFormat_setInt32(format, AMEDIAFORMAT_KEY_FRAME_RATE, fps);
  997 + AMediaFormat_setInt32(format, AMEDIAFORMAT_KEY_I_FRAME_INTERVAL, 1);
  998 + AMediaFormat_setInt32(format, AMEDIAFORMAT_KEY_BIT_RATE, 2000000);
  999 + AMediaFormat_setInt32(format, AMEDIAFORMAT_KEY_COLOR_FORMAT,
  1000 + 21); // COLOR_FormatYUV420SemiPlanar
  1001 +
  1002 + // 配置编码器
  1003 + media_status_t status = AMediaCodec_configure(video_encoder, format,
  1004 + nullptr, nullptr,
  1005 + AMEDIACODEC_CONFIGURE_FLAG_ENCODE);
  1006 + AMediaFormat_delete(format);
  1007 +
  1008 + if (status != AMEDIA_OK) {
  1009 + __android_log_print(ANDROID_LOG_ERROR, "NdkCamera", "Failed to configure video encoder: %d", status);
  1010 + AMediaCodec_delete(video_encoder);
  1011 + video_encoder = nullptr;
  1012 + return false;
  1013 + }
  1014 +
  1015 + // 启动编码器
  1016 + status = AMediaCodec_start(video_encoder);
  1017 + if (status != AMEDIA_OK) {
  1018 + __android_log_print(ANDROID_LOG_ERROR, "NdkCamera", "Failed to start video encoder: %d", status);
  1019 + AMediaCodec_delete(video_encoder);
  1020 + video_encoder = nullptr;
  1021 + return false;
  1022 + }
  1023 +
  1024 + __android_log_print(ANDROID_LOG_INFO, "NdkCamera", "Video encoder setup: %dx%d %dfps", width, height, fps);
  1025 + return true;
  1026 +}
  1027 +
  1028 +bool NdkCameraWindow::setup_audio_encoder() {
  1029 + // 创建音频编码器
  1030 + const char* mime_type = "audio/mp4a-latm";
  1031 + audio_encoder = AMediaCodec_createEncoderByType(mime_type);
  1032 + if (!audio_encoder) {
  1033 + __android_log_print(ANDROID_LOG_ERROR, "NdkCamera", "Failed to create audio encoder");
  1034 + return false;
  1035 + }
  1036 +
  1037 + // 配置音频格式
  1038 + AMediaFormat* format = AMediaFormat_new();
  1039 + AMediaFormat_setString(format, AMEDIAFORMAT_KEY_MIME, mime_type);
  1040 + AMediaFormat_setInt32(format, AMEDIAFORMAT_KEY_SAMPLE_RATE, 44100);
  1041 + AMediaFormat_setInt32(format, AMEDIAFORMAT_KEY_CHANNEL_COUNT, 1);
  1042 + AMediaFormat_setInt32(format, AMEDIAFORMAT_KEY_BIT_RATE, 128000);
  1043 + AMediaFormat_setInt32(format, AMEDIAFORMAT_KEY_AAC_PROFILE, 2); // AAC LC
  1044 +
  1045 + // 配置编码器
  1046 + media_status_t status = AMediaCodec_configure(audio_encoder, format,
  1047 + nullptr, nullptr,
  1048 + AMEDIACODEC_CONFIGURE_FLAG_ENCODE);
  1049 + AMediaFormat_delete(format);
  1050 +
  1051 + if (status != AMEDIA_OK) {
  1052 + __android_log_print(ANDROID_LOG_ERROR, "NdkCamera", "Failed to configure audio encoder: %d", status);
  1053 + AMediaCodec_delete(audio_encoder);
  1054 + audio_encoder = nullptr;
  1055 + return false;
  1056 + }
  1057 +
  1058 + // 启动编码器
  1059 + status = AMediaCodec_start(audio_encoder);
  1060 + if (status != AMEDIA_OK) {
  1061 + __android_log_print(ANDROID_LOG_ERROR, "NdkCamera", "Failed to start audio encoder: %d", status);
  1062 + AMediaCodec_delete(audio_encoder);
  1063 + audio_encoder = nullptr;
  1064 + return false;
  1065 + }
  1066 +
  1067 + __android_log_print(ANDROID_LOG_INFO, "NdkCamera", "Audio encoder setup");
  1068 + return true;
  1069 +}
  1070 +
  1071 +void NdkCameraWindow::encode_video_frame(const unsigned char* nv21_data, int width, int height, int64_t timestamp_us) {
  1072 + if (!video_encoder) {
  1073 + return;
  1074 + }
  1075 +
  1076 + // 检查录制状态
  1077 + {
  1078 + std::lock_guard<std::mutex> lock(recording_mutex);
  1079 + if (!recording_active) {
  1080 + return;
  1081 + }
  1082 + }
  1083 +
  1084 + // 限制队列大小,避免内存溢出
  1085 + const size_t max_queue_size = 30; // 最多缓存30帧
  1086 +
  1087 + std::lock_guard<std::mutex> lock(recording_mutex);
  1088 + if (video_frame_queue.size() >= max_queue_size) {
  1089 + // 队列已满,丢弃最旧的一帧
  1090 + video_frame_queue.pop();
  1091 + __android_log_print(ANDROID_LOG_WARN, "NdkCamera", "Video frame queue full, dropping frame");
  1092 + }
  1093 +
  1094 + // 将视频帧添加到队列供录制线程处理
  1095 + std::vector<uint8_t> frame_data(nv21_data, nv21_data + width * height * 3 / 2);
  1096 + video_frame_queue.push(std::move(frame_data));
  1097 +}
  1098 +
  1099 +void NdkCameraWindow::encode_video_frame_data(const uint8_t* data, size_t size) {
  1100 + if (!video_encoder || !recording_active) {
  1101 + return;
  1102 + }
  1103 +
  1104 + // 获取输入缓冲区
  1105 + ssize_t input_index = AMediaCodec_dequeueInputBuffer(video_encoder, 10000);
  1106 + if (input_index >= 0) {
  1107 + size_t buffer_size;
  1108 + uint8_t* buffer = AMediaCodec_getInputBuffer(video_encoder, input_index, &buffer_size);
  1109 +
  1110 + if (buffer && buffer_size >= size) {
  1111 + // 复制数据到缓冲区
  1112 + memcpy(buffer, data, size);
  1113 +
  1114 + // 提交缓冲区给编码器
  1115 + media_status_t status = AMediaCodec_queueInputBuffer(video_encoder, input_index,
  1116 + 0, size,
  1117 + get_current_timestamp(), 0);
  1118 + if (status != AMEDIA_OK) {
  1119 + __android_log_print(ANDROID_LOG_ERROR, "NdkCamera", "Failed to queue input buffer: %d", status);
  1120 + } else {
  1121 + __android_log_print(ANDROID_LOG_DEBUG, "NdkCamera", "Queued input buffer: index=%zd, size=%zu",
  1122 + input_index, size);
  1123 + }
  1124 + } else {
  1125 + __android_log_print(ANDROID_LOG_WARN, "NdkCamera", "Input buffer too small: need=%zu, have=%zu",
  1126 + size, buffer_size);
  1127 + }
  1128 + } else if (input_index == AMEDIACODEC_INFO_TRY_AGAIN_LATER) {
  1129 + __android_log_print(ANDROID_LOG_DEBUG, "NdkCamera", "No input buffer available, try again later");
  1130 + } else {
  1131 + __android_log_print(ANDROID_LOG_ERROR, "NdkCamera", "Failed to get input buffer: %zd", input_index);
  1132 + }
  1133 +}
  1134 +
  1135 +void NdkCameraWindow::process_encoder_output() {
  1136 + if (!video_encoder || !recording_active) {
  1137 + return;
  1138 + }
  1139 +
  1140 + // 处理输出缓冲区
  1141 + AMediaCodecBufferInfo info;
  1142 + ssize_t output_index = AMediaCodec_dequeueOutputBuffer(video_encoder, &info, 10000); // 10ms超时
  1143 + while (output_index >= 0) {
  1144 + if (info.flags & AMEDIACODEC_BUFFER_FLAG_CODEC_CONFIG) {
  1145 + // 配置数据
  1146 + if (!muxer_started && media_muxer) {
  1147 + // 添加视频轨道
  1148 + AMediaFormat* format = AMediaCodec_getOutputFormat(video_encoder);
  1149 + if (format) {
  1150 + video_track_index = AMediaMuxer_addTrack(media_muxer, format);
  1151 + AMediaFormat_delete(format);
  1152 +
  1153 + __android_log_print(ANDROID_LOG_INFO, "NdkCamera", "Video track added: %d", video_track_index);
  1154 +
  1155 + // 如果音频轨道也已准备好,启动混合器
  1156 + if (audio_track_index >= 0 || !audio_encoder) {
  1157 + media_status_t status = AMediaMuxer_start(media_muxer);
  1158 + if (status == AMEDIA_OK) {
  1159 + muxer_started = true;
  1160 + __android_log_print(ANDROID_LOG_INFO, "NdkCamera", "MediaMuxer started");
  1161 + } else {
  1162 + __android_log_print(ANDROID_LOG_ERROR, "NdkCamera", "Failed to start MediaMuxer: %d", status);
  1163 + }
  1164 + }
  1165 + }
  1166 + }
  1167 + } else if (info.size > 0) {
  1168 + // 编码数据
  1169 + if (muxer_started && media_muxer) {
  1170 + size_t buffer_size;
  1171 + uint8_t* buffer = AMediaCodec_getOutputBuffer(video_encoder, output_index, &buffer_size);
  1172 + if (buffer) {
  1173 + media_status_t status = AMediaMuxer_writeSampleData(media_muxer, video_track_index, buffer, &info);
  1174 + if (status != AMEDIA_OK) {
  1175 + __android_log_print(ANDROID_LOG_ERROR, "NdkCamera", "Failed to write sample data: %d", status);
  1176 + } else {
  1177 + __android_log_print(ANDROID_LOG_DEBUG, "NdkCamera", "Wrote sample data: size=%d, pts=%ld",
  1178 + info.size, info.presentationTimeUs);
  1179 + }
  1180 + }
  1181 + }
  1182 + }
  1183 +
  1184 + AMediaCodec_releaseOutputBuffer(video_encoder, output_index, false);
  1185 + output_index = AMediaCodec_dequeueOutputBuffer(video_encoder, &info, 10000);
  1186 + }
  1187 +}
  1188 +
  1189 +void NdkCameraWindow::process_encoder_output_final() {
  1190 + if (!video_encoder) {
  1191 + return;
  1192 + }
  1193 +
  1194 + // 处理所有剩余的编码器输出
  1195 + AMediaCodecBufferInfo info;
  1196 + ssize_t output_index = AMediaCodec_dequeueOutputBuffer(video_encoder, &info, 1000000); // 1秒超时
  1197 + while (output_index >= 0) {
  1198 + if (info.size > 0 && muxer_started && media_muxer) {
  1199 + size_t buffer_size;
  1200 + uint8_t* buffer = AMediaCodec_getOutputBuffer(video_encoder, output_index, &buffer_size);
  1201 + if (buffer) {
  1202 + AMediaMuxer_writeSampleData(media_muxer, video_track_index, buffer, &info);
  1203 + }
  1204 + }
  1205 +
  1206 + AMediaCodec_releaseOutputBuffer(video_encoder, output_index, false);
  1207 + output_index = AMediaCodec_dequeueOutputBuffer(video_encoder, &info, 1000000);
  1208 + }
  1209 +}
  1210 +
  1211 +int64_t NdkCameraWindow::get_current_timestamp() {
  1212 + struct timeval tv;
  1213 + gettimeofday(&tv, nullptr);
  1214 + return (int64_t)tv.tv_sec * 1000000 + tv.tv_usec;
  1215 +}
@@ -22,6 +22,12 @@ @@ -22,6 +22,12 @@
22 #include <camera/NdkCameraManager.h> 22 #include <camera/NdkCameraManager.h>
23 #include <camera/NdkCameraMetadata.h> 23 #include <camera/NdkCameraMetadata.h>
24 #include <media/NdkImageReader.h> 24 #include <media/NdkImageReader.h>
  25 +#include <media/NdkMediaCodec.h>
  26 +#include <media/NdkMediaMuxer.h>
  27 +#include <media/NdkMediaFormat.h>
  28 +#include <thread>
  29 +#include <mutex>
  30 +#include <queue>
25 31
26 #include <opencv2/core/core.hpp> 32 #include <opencv2/core/core.hpp>
27 33
@@ -67,6 +73,11 @@ public: @@ -67,6 +73,11 @@ public:
67 73
68 virtual void on_image(const unsigned char* nv21, int nv21_width, int nv21_height) const; 74 virtual void on_image(const unsigned char* nv21, int nv21_width, int nv21_height) const;
69 75
  76 + // 录制相关方法
  77 + bool startRecording(const char* filepath);
  78 + bool stopRecording();
  79 + bool isRecording() const;
  80 +
70 public: 81 public:
71 mutable int accelerometer_orientation; 82 mutable int accelerometer_orientation;
72 83
@@ -75,6 +86,30 @@ private: @@ -75,6 +86,30 @@ private:
75 mutable ASensorEventQueue* sensor_event_queue; 86 mutable ASensorEventQueue* sensor_event_queue;
76 const ASensor* accelerometer_sensor; 87 const ASensor* accelerometer_sensor;
77 ANativeWindow* win; 88 ANativeWindow* win;
  89 +
  90 + // 录制相关成员变量
  91 + AMediaCodec* video_encoder;
  92 + AMediaCodec* audio_encoder;
  93 + AMediaMuxer* media_muxer;
  94 + std::thread* recording_thread;
  95 + std::mutex recording_mutex;
  96 + std::queue<std::vector<uint8_t>> video_frame_queue;
  97 + bool recording_active;
  98 + int video_track_index;
  99 + int audio_track_index;
  100 + bool muxer_started;
  101 +
  102 + void recording_worker();
  103 + bool setup_video_encoder(int width, int height, int fps);
  104 + bool setup_audio_encoder();
  105 + void encode_video_frame(const unsigned char* nv21_data, int width, int height, int64_t timestamp_us);
  106 + void encode_video_frame_data(const uint8_t* data, size_t size);
  107 + int64_t get_current_timestamp();
  108 + void process_encoder_output();
  109 + void process_encoder_output_final();
  110 +
  111 +private:
  112 + std::string recording_filepath;
78 }; 113 };
79 114
80 #endif // NDKCAMERA_H 115 #endif // NDKCAMERA_H
@@ -22,6 +22,9 @@ @@ -22,6 +22,9 @@
22 22
23 #include <string> 23 #include <string>
24 #include <vector> 24 #include <vector>
  25 +#include <thread>
  26 +#include <mutex>
  27 +#include <queue>
25 28
26 #include <platform.h> 29 #include <platform.h>
27 #include <benchmark.h> 30 #include <benchmark.h>
@@ -33,6 +36,10 @@ @@ -33,6 +36,10 @@
33 #include <opencv2/core/core.hpp> 36 #include <opencv2/core/core.hpp>
34 #include <opencv2/imgproc/imgproc.hpp> 37 #include <opencv2/imgproc/imgproc.hpp>
35 38
  39 +#include <media/NdkMediaCodec.h>
  40 +#include <media/NdkMediaMuxer.h>
  41 +#include <media/NdkMediaFormat.h>
  42 +
36 #if __ARM_NEON 43 #if __ARM_NEON
37 #include <arm_neon.h> 44 #include <arm_neon.h>
38 #endif // __ARM_NEON 45 #endif // __ARM_NEON
@@ -298,4 +305,39 @@ JNIEXPORT jboolean JNICALL Java_com_tencent_yolo11ncnn_YOLO11Ncnn_setOutputWindo @@ -298,4 +305,39 @@ JNIEXPORT jboolean JNICALL Java_com_tencent_yolo11ncnn_YOLO11Ncnn_setOutputWindo
298 return JNI_TRUE; 305 return JNI_TRUE;
299 } 306 }
300 307
  308 +// public native boolean startRecording(String filepath);
  309 +JNIEXPORT jboolean JNICALL Java_com_tencent_yolo11ncnn_YOLO11Ncnn_startRecording(JNIEnv* env, jobject thiz, jstring filepath)
  310 +{
  311 + const char* filepath_str = env->GetStringUTFChars(filepath, nullptr);
  312 + if (!filepath_str) {
  313 + return JNI_FALSE;
  314 + }
  315 +
  316 + __android_log_print(ANDROID_LOG_DEBUG, "ncnn", "startRecording %s", filepath_str);
  317 +
  318 + bool result = g_camera->startRecording(filepath_str);
  319 +
  320 + env->ReleaseStringUTFChars(filepath, filepath_str);
  321 +
  322 + return result ? JNI_TRUE : JNI_FALSE;
  323 +}
  324 +
  325 +// public native boolean stopRecording();
  326 +JNIEXPORT jboolean JNICALL Java_com_tencent_yolo11ncnn_YOLO11Ncnn_stopRecording(JNIEnv* env, jobject thiz)
  327 +{
  328 + __android_log_print(ANDROID_LOG_DEBUG, "ncnn", "stopRecording");
  329 +
  330 + bool result = g_camera->stopRecording();
  331 +
  332 + return result ? JNI_TRUE : JNI_FALSE;
  333 +}
  334 +
  335 +// public native boolean isRecording();
  336 +JNIEXPORT jboolean JNICALL Java_com_tencent_yolo11ncnn_YOLO11Ncnn_isRecording(JNIEnv* env, jobject thiz)
  337 +{
  338 + bool result = g_camera->isRecording();
  339 +
  340 + return result ? JNI_TRUE : JNI_FALSE;
  341 +}
  342 +
301 } 343 }
@@ -15,6 +15,12 @@ @@ -15,6 +15,12 @@
15 android:layout_height="wrap_content" 15 android:layout_height="wrap_content"
16 android:text="切换摄像头" /> 16 android:text="切换摄像头" />
17 17
  18 + <Button
  19 + android:id="@+id/buttonRecord"
  20 + android:layout_width="wrap_content"
  21 + android:layout_height="wrap_content"
  22 + android:text="开始录制" />
  23 +
18 </LinearLayout> 24 </LinearLayout>
19 25
20 <LinearLayout 26 <LinearLayout