正在显示
8 个修改的文件
包含
566 行增加
和
0 行删除
| @@ -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"); |
| @@ -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 |
-
请 注册 或 登录 后发表评论