Skip to content

Commit 420f424

Browse files
committed
fix: Color works + 30FPS
1 parent 6a160ce commit 420f424

2 files changed

Lines changed: 127 additions & 61 deletions

File tree

‎app/src/main/cpp/camera_stream_manager.cpp‎

Lines changed: 86 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ namespace {
4343
static constexpr const char* TAG = "CameraStreamManager";
4444
static constexpr int PREVIEW_SELECT_TIMEOUT_US = 100000;
4545
static constexpr int STOP_WAIT_MS = 2500;
46+
static constexpr int COLOR_FORMAT_YUV420_PLANAR = 19;
47+
static constexpr int COLOR_FORMAT_YUV420_SEMIPLANAR = 21;
4648

4749
void logPrint(int level, const char* fmt, va_list args) {
4850
__android_log_vprint(level, TAG, fmt, args);
@@ -125,6 +127,20 @@ struct MappedBuffer {
125127
size_t length = 0;
126128
};
127129

130+
void packI420ToNv12(const uint8_t* src, uint8_t* dst, int width, int height) {
131+
const size_t ySize = static_cast<size_t>(width) * static_cast<size_t>(height);
132+
const size_t uvPlaneSize = ySize / 4U;
133+
const uint8_t* srcY = src;
134+
const uint8_t* srcU = src + ySize;
135+
const uint8_t* srcV = srcU + uvPlaneSize;
136+
std::memcpy(dst, srcY, ySize);
137+
uint8_t* dstUv = dst + ySize;
138+
for (size_t i = 0; i < uvPlaneSize; i++) {
139+
dstUv[i * 2U] = srcU[i];
140+
dstUv[i * 2U + 1U] = srcV[i];
141+
}
142+
}
143+
128144
class RecordingSink {
129145
public:
130146
RecordingSink(int slot, int videoIndex, std::string outputPath, int requestedWidth,
@@ -150,34 +166,7 @@ class RecordingSink {
150166
return false;
151167
}
152168

153-
codec_ = AMediaCodec_createEncoderByType("video/avc");
154-
if (!codec_) {
155-
loge("slot=%d AMediaCodec_createEncoderByType failed", slot_);
156-
return false;
157-
}
158-
159-
AMediaFormat* format = AMediaFormat_new();
160-
AMediaFormat_setString(format, AMEDIAFORMAT_KEY_MIME, "video/avc");
161-
AMediaFormat_setInt32(format, AMEDIAFORMAT_KEY_WIDTH, recWidth_);
162-
AMediaFormat_setInt32(format, AMEDIAFORMAT_KEY_HEIGHT, recHeight_);
163-
AMediaFormat_setInt32(format, AMEDIAFORMAT_KEY_BIT_RATE, bitrate_);
164-
AMediaFormat_setInt32(format, AMEDIAFORMAT_KEY_FRAME_RATE, fps_);
165-
AMediaFormat_setInt32(format, AMEDIAFORMAT_KEY_I_FRAME_INTERVAL, 1);
166-
AMediaFormat_setInt32(format, AMEDIAFORMAT_KEY_COLOR_FORMAT, 19);
167-
168-
media_status_t status = AMediaCodec_configure(
169-
codec_, format, nullptr, nullptr, AMEDIACODEC_CONFIGURE_FLAG_ENCODE);
170-
AMediaFormat_delete(format);
171-
if (status != AMEDIA_OK) {
172-
loge("slot=%d AMediaCodec_configure failed: %d", slot_, static_cast<int>(status));
173-
finalize();
174-
return false;
175-
}
176-
177-
status = AMediaCodec_start(codec_);
178-
if (status != AMEDIA_OK) {
179-
loge("slot=%d AMediaCodec_start failed: %d", slot_, static_cast<int>(status));
180-
finalize();
169+
if (!initializeCodec()) {
181170
return false;
182171
}
183172

@@ -196,10 +185,12 @@ class RecordingSink {
196185
}
197186

198187
i420Frame_.create(recHeight_ + (recHeight_ / 2), recWidth_, CV_8UC1);
188+
encoderFrame_.resize(static_cast<size_t>(recWidth_) * static_cast<size_t>(recHeight_) * 3U / 2U);
199189
frameDurationUs_ = 1000000LL / std::max(1, fps_);
200190
startUs_ = nowUs();
201191
nextPtsUs_ = 0;
202192
frameCount_ = 0;
193+
logi("slot=%d /dev/video%d encoder color format=%d", slot_, videoIndex_, encoderColorFormat_);
203194
return true;
204195
}
205196

@@ -239,14 +230,19 @@ class RecordingSink {
239230

240231
cv::Mat rgbaCrop = rgbaFrame(cv::Rect(0, 0, recWidth_, recHeight_));
241232
cv::cvtColor(rgbaCrop, i420Frame_, cv::COLOR_RGBA2YUV_I420);
233+
if (encoderColorFormat_ == COLOR_FORMAT_YUV420_SEMIPLANAR) {
234+
packI420ToNv12(i420Frame_.data, encoderFrame_.data(), recWidth_, recHeight_);
235+
} else {
236+
std::memcpy(encoderFrame_.data(), i420Frame_.data, encoderFrame_.size());
237+
}
242238

243239
ssize_t inputIndex = AMediaCodec_dequeueInputBuffer(codec_, 10000);
244240
if (inputIndex >= 0) {
245241
size_t inputSize = 0;
246242
uint8_t* inputBuffer = AMediaCodec_getInputBuffer(codec_, static_cast<size_t>(inputIndex), &inputSize);
247-
const size_t frameSize = static_cast<size_t>(recWidth_) * static_cast<size_t>(recHeight_) * 3U / 2U;
243+
const size_t frameSize = encoderFrame_.size();
248244
if (inputBuffer != nullptr && inputSize >= frameSize) {
249-
std::memcpy(inputBuffer, i420Frame_.data, frameSize);
245+
std::memcpy(inputBuffer, encoderFrame_.data(), frameSize);
250246
const int64_t pts = frameCount_ * frameDurationUs_;
251247
if (AMediaCodec_queueInputBuffer(codec_, static_cast<size_t>(inputIndex), 0, frameSize,
252248
static_cast<uint64_t>(pts), 0) == AMEDIA_OK) {
@@ -302,6 +298,64 @@ class RecordingSink {
302298
}
303299

304300
private:
301+
bool initializeCodec() {
302+
const int preferredFormats[] = {
303+
COLOR_FORMAT_YUV420_SEMIPLANAR,
304+
COLOR_FORMAT_YUV420_PLANAR
305+
};
306+
for (int colorFormat : preferredFormats) {
307+
if (tryInitializeCodec(colorFormat)) {
308+
encoderColorFormat_ = colorFormat;
309+
return true;
310+
}
311+
releaseCodecOnly();
312+
}
313+
loge("slot=%d no usable encoder color format found", slot_);
314+
return false;
315+
}
316+
317+
bool tryInitializeCodec(int colorFormat) {
318+
codec_ = AMediaCodec_createEncoderByType("video/avc");
319+
if (!codec_) {
320+
loge("slot=%d AMediaCodec_createEncoderByType failed", slot_);
321+
return false;
322+
}
323+
324+
AMediaFormat* format = AMediaFormat_new();
325+
AMediaFormat_setString(format, AMEDIAFORMAT_KEY_MIME, "video/avc");
326+
AMediaFormat_setInt32(format, AMEDIAFORMAT_KEY_WIDTH, recWidth_);
327+
AMediaFormat_setInt32(format, AMEDIAFORMAT_KEY_HEIGHT, recHeight_);
328+
AMediaFormat_setInt32(format, AMEDIAFORMAT_KEY_BIT_RATE, bitrate_);
329+
AMediaFormat_setInt32(format, AMEDIAFORMAT_KEY_FRAME_RATE, fps_);
330+
AMediaFormat_setInt32(format, AMEDIAFORMAT_KEY_I_FRAME_INTERVAL, 1);
331+
AMediaFormat_setInt32(format, AMEDIAFORMAT_KEY_COLOR_FORMAT, colorFormat);
332+
333+
media_status_t status = AMediaCodec_configure(
334+
codec_, format, nullptr, nullptr, AMEDIACODEC_CONFIGURE_FLAG_ENCODE);
335+
AMediaFormat_delete(format);
336+
if (status != AMEDIA_OK) {
337+
logw("slot=%d AMediaCodec_configure failed for color format %d: %d",
338+
slot_, colorFormat, static_cast<int>(status));
339+
return false;
340+
}
341+
342+
status = AMediaCodec_start(codec_);
343+
if (status != AMEDIA_OK) {
344+
logw("slot=%d AMediaCodec_start failed for color format %d: %d",
345+
slot_, colorFormat, static_cast<int>(status));
346+
return false;
347+
}
348+
return true;
349+
}
350+
351+
void releaseCodecOnly() {
352+
if (codec_ != nullptr) {
353+
AMediaCodec_delete(codec_);
354+
codec_ = nullptr;
355+
}
356+
encoderColorFormat_ = COLOR_FORMAT_YUV420_PLANAR;
357+
}
358+
305359
void queueEndOfStream() {
306360
if (eosQueued_ || codec_ == nullptr) {
307361
return;
@@ -378,6 +432,7 @@ class RecordingSink {
378432
int64_t startUs_ = 0;
379433
int64_t nextPtsUs_ = 0;
380434
int64_t frameCount_ = 0;
435+
int encoderColorFormat_ = COLOR_FORMAT_YUV420_PLANAR;
381436

382437
AMediaCodec* codec_ = nullptr;
383438
AMediaMuxer* muxer_ = nullptr;
@@ -387,6 +442,7 @@ class RecordingSink {
387442
bool eosQueued_ = false;
388443

389444
cv::Mat i420Frame_;
445+
std::vector<uint8_t> encoderFrame_;
390446

391447
std::atomic<bool> stopRequested_{false};
392448
std::atomic<bool> finalized_{false};

‎app/src/main/java/com/drivehub/kamera/RecordingService.java‎

Lines changed: 41 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public class RecordingService extends Service {
5050
private static final String CHANNEL_ID = "mg4_recording";
5151
private static final int NOTIF_ID = 42;
5252
private static final int TOTAL_CAMERAS = 4;
53-
private static final long TEST_RECORDING_MS = 30_000L;
53+
private static final long TEST_RECORDING_MS = 10_000L;
5454

5555
private static volatile boolean sServiceRunning = false;
5656

@@ -64,7 +64,8 @@ public static boolean isRunning() {
6464
public static void startIfNeeded(Context context) {
6565
SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, MODE_PRIVATE);
6666
boolean enabled = prefs.getBoolean(KEY_ENABLED, false);
67-
if (!enabled) return;
67+
if (!enabled)
68+
return;
6869
Intent i = new Intent(context, RecordingService.class);
6970
i.setAction(ACTION_START);
7071
context.startForegroundService(i);
@@ -91,7 +92,8 @@ public void onCreate() {
9192

9293
@Override
9394
public int onStartCommand(Intent intent, int flags, int startId) {
94-
if (intent == null) return START_STICKY;
95+
if (intent == null)
96+
return START_STICKY;
9597
String action = intent.getAction();
9698

9799
if (ACTION_STOP.equals(action)) {
@@ -134,7 +136,7 @@ public int onStartCommand(Intent intent, int flags, int startId) {
134136

135137
private void recordTestClip() {
136138
File baseDir = getRecordsBaseDir();
137-
//noinspection ResultOfMethodCallIgnored
139+
// noinspection ResultOfMethodCallIgnored
138140
baseDir.mkdirs();
139141
if (!baseDir.exists() || !baseDir.canWrite()) {
140142
publishStatus(STATUS_ERROR, 0, TOTAL_CAMERAS, "storage not writable");
@@ -144,7 +146,8 @@ private void recordTestClip() {
144146
return;
145147
}
146148

147-
boolean startedAnyCamera = recordClip(baseDir, TEST_RECORDING_MS, "test_" + makeTimestampBaseWithSeconds(System.currentTimeMillis()), -1);
149+
boolean startedAnyCamera = recordClip(baseDir, TEST_RECORDING_MS,
150+
"test_" + makeTimestampBaseWithSeconds(System.currentTimeMillis()), -1);
148151
worker = null;
149152
if (startedAnyCamera) {
150153
publishStatus(STATUS_OFF, 0, TOTAL_CAMERAS, "");
@@ -168,7 +171,7 @@ private void recordLoop() {
168171
}
169172

170173
File baseDir = getRecordsBaseDir();
171-
//noinspection ResultOfMethodCallIgnored
174+
// noinspection ResultOfMethodCallIgnored
172175
baseDir.mkdirs();
173176
if (!baseDir.exists() || !baseDir.canWrite()) {
174177
publishStatus(STATUS_ERROR, 0, TOTAL_CAMERAS, "storage not writable");
@@ -179,20 +182,23 @@ private void recordLoop() {
179182

180183
long segmentMs = segmentMin * 60L * 1000L;
181184

182-
// Convert total duration into a segment count: segmentMin=3, totalMin=30 => keep 10 segments.
185+
// Convert total duration into a segment count: segmentMin=3, totalMin=30 =>
186+
// keep 10 segments.
183187
int keepSegments = Math.max(1, totalMin / segmentMin);
184188
boolean endedWithFatalError = false;
185189

186190
while (!stopRequested) {
187-
boolean startedAnyCamera = recordClip(baseDir, segmentMs, makeTimestampBase(System.currentTimeMillis()), keepSegments);
191+
boolean startedAnyCamera = recordClip(baseDir, segmentMs, makeTimestampBase(System.currentTimeMillis()),
192+
keepSegments);
188193
if (!startedAnyCamera) {
189194
endedWithFatalError = true;
190195
break;
191196
}
192197

193198
// Check whether recording has been disabled in prefs.
194199
enabled = prefs.getBoolean(KEY_ENABLED, false);
195-
if (!enabled) break;
200+
if (!enabled)
201+
break;
196202
}
197203

198204
worker = null;
@@ -206,12 +212,11 @@ private void recordLoop() {
206212
private boolean recordClip(File baseDir, long durationMs, String baseName, int keepSegments) {
207213
// Output names for the 4 cameras.
208214
// F = v15 (front), R = v17 (rear), X = v16 (left), Y = v14 (right)
209-
int[] slots = new int[]{0, 1, 2, 3};
210-
int[] videoIndices = new int[]{15, 17, 16, 14};
211-
char[] names = new char[]{'F', 'R', 'X', 'Y'};
215+
int[] slots = new int[] { 0, 1, 2, 3 };
216+
int[] videoIndices = new int[] { 15, 17, 16, 14 };
217+
char[] names = new char[] { 'F', 'R', 'X', 'Y' };
212218
int recordingFps = DashcamSettingsController.getRecordingFps(
213-
getSharedPreferences(PREFS_NAME, MODE_PRIVATE)
214-
);
219+
getSharedPreferences(PREFS_NAME, MODE_PRIVATE));
215220

216221
String[] outPaths = new String[4];
217222
for (int i = 0; i < 4; i++) {
@@ -224,12 +229,12 @@ private boolean recordClip(File baseDir, long durationMs, String baseName, int k
224229
StringBuilder failedCameras = new StringBuilder();
225230
for (int i = 0; i < 4; i++) {
226231
boolean started = CameraProbe.startMp4Record(
227-
slots[i], videoIndices[i], outPaths[i], 720, 240, recordingFps, 2500000
228-
);
232+
slots[i], videoIndices[i], outPaths[i], 720, 240, recordingFps, 2500000);
229233
if (started) {
230234
activeCameras++;
231235
} else {
232-
if (failedCameras.length() > 0) failedCameras.append(", ");
236+
if (failedCameras.length() > 0)
237+
failedCameras.append(", ");
233238
failedCameras.append(names[i]).append(" /dev/video").append(videoIndices[i]);
234239
}
235240
}
@@ -265,16 +270,19 @@ private boolean recordClip(File baseDir, long durationMs, String baseName, int k
265270

266271
private void cleanupOldSegments(File baseDir, int keepSegments) {
267272
File[] files = baseDir.listFiles();
268-
if (files == null) return;
273+
if (files == null)
274+
return;
269275

270276
// baseName => earliestModified
271277
Map<String, Long> groupTime = new HashMap<>();
272278
for (File f : files) {
273279
String name = f.getName();
274-
if (!name.endsWith(".mp4")) continue;
280+
if (!name.endsWith(".mp4"))
281+
continue;
275282
// yymmddhhmm_X.mp4
276283
int underscore = name.indexOf('_');
277-
if (underscore <= 0) continue;
284+
if (underscore <= 0)
285+
continue;
278286
String base = name.substring(0, underscore);
279287
long t = f.lastModified();
280288
groupTime.merge(base, t, Math::min);
@@ -283,15 +291,16 @@ private void cleanupOldSegments(File baseDir, int keepSegments) {
283291
List<Map.Entry<String, Long>> groups = new ArrayList<>(groupTime.entrySet());
284292
groups.sort(Comparator.comparingLong(Map.Entry::getValue));
285293

286-
if (groups.size() <= keepSegments) return;
294+
if (groups.size() <= keepSegments)
295+
return;
287296
int deleteCount = groups.size() - keepSegments;
288297

289-
char[] suffixes = new char[]{'F', 'R', 'X', 'Y'};
298+
char[] suffixes = new char[] { 'F', 'R', 'X', 'Y' };
290299
for (int i = 0; i < deleteCount; i++) {
291300
String base = groups.get(i).getKey();
292301
for (char s : suffixes) {
293302
File f = new File(baseDir, base + "_" + s + ".mp4");
294-
//noinspection ResultOfMethodCallIgnored
303+
// noinspection ResultOfMethodCallIgnored
295304
f.delete();
296305
}
297306
}
@@ -301,7 +310,7 @@ private File getRecordsBaseDir() {
301310
// Android 9: write directly into the Downloads folder.
302311
File downloads = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
303312
File dir = new File(downloads, "mg4_cam_records");
304-
//noinspection ResultOfMethodCallIgnored
313+
// noinspection ResultOfMethodCallIgnored
305314
dir.mkdirs();
306315
return dir;
307316
}
@@ -345,13 +354,14 @@ private void publishCurrentStatus() {
345354
prefs.getString(KEY_STATUS, STATUS_OFF),
346355
prefs.getInt(KEY_ACTIVE_CAMERAS, 0),
347356
prefs.getInt(KEY_TOTAL_CAMERAS, TOTAL_CAMERAS),
348-
prefs.getString(KEY_LAST_ERROR, "")
349-
);
357+
prefs.getString(KEY_LAST_ERROR, ""));
350358
}
351359

352360
private void publishStatus(String status, int activeCameras, int totalCameras, String lastError) {
353-
if (status == null) status = STATUS_OFF;
354-
if (lastError == null) lastError = "";
361+
if (status == null)
362+
status = STATUS_OFF;
363+
if (lastError == null)
364+
lastError = "";
355365
getSharedPreferences(PREFS_NAME, MODE_PRIVATE).edit()
356366
.putString(KEY_STATUS, status)
357367
.putInt(KEY_ACTIVE_CAMERAS, Math.max(0, activeCameras))
@@ -390,10 +400,10 @@ private void createNotificationChannel() {
390400
NotificationChannel ch = new NotificationChannel(
391401
CHANNEL_ID,
392402
getString(R.string.notification_channel_recording),
393-
NotificationManager.IMPORTANCE_LOW
394-
);
403+
NotificationManager.IMPORTANCE_LOW);
395404
NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
396-
if (nm != null) nm.createNotificationChannel(ch);
405+
if (nm != null)
406+
nm.createNotificationChannel(ch);
397407
}
398408

399409
@Nullable

0 commit comments

Comments
 (0)