Compare commits

...

6 Commits

Author SHA1 Message Date
31e7b8bd35 Merge pull request 'Catch focus timeout and negative value errs' (#21) from loewy/fix-android-focus-timeout-crash into main
Reviewed-on: #21
2026-01-27 00:50:04 +00:00
3bb72d5d94 catch negative values set by out of preview bound press 2026-01-21 12:58:13 -08:00
ac5dac127f catch focus timeout error on android 2026-01-21 12:58:13 -08:00
e3de8c018c Merge pull request 'WIP fix: Skip NAL header byte when reading SPS profile data in HlsMuxer' (#20) from fix/hlsmuxer-codec-string into main
Reviewed-on: #20
2026-01-21 20:57:30 +00:00
Dean
dd26812a9c fix: Add pasp box to declare square pixels (1:1) for web playback
The codec string fix caused videos to appear squished on web players
like Shaka. Adding an explicit pixel aspect ratio (pasp) box with
1:1 ratio tells the player not to apply any SAR scaling.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-13 12:22:43 -08:00
Dean
b716608379 fix: Skip NAL header byte when reading SPS profile data in HlsMuxer
The SPS NAL unit format is: [NAL header, profile_idc, constraint_flags, level_idc, ...]
The code was incorrectly reading from byte 0 (NAL header, typically 0x67)
instead of byte 1 (profile_idc).

This produced invalid codec strings like `avc1.676400` instead of valid
ones like `avc1.64001f`, causing Shaka Player on web to fail with error
4032 (unable to parse codec).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-13 11:52:08 -08:00
3 changed files with 31 additions and 9 deletions

View File

@@ -753,17 +753,32 @@ class HlsMuxer(
dos.writeShort(-1) // pre-defined dos.writeShort(-1) // pre-defined
output.write(buildAvcCBox(sps, pps)) output.write(buildAvcCBox(sps, pps))
output.write(buildPaspBox())
return wrapBox("avc1", output.toByteArray()) return wrapBox("avc1", output.toByteArray())
} }
/**
* Builds pixel aspect ratio box to explicitly declare square pixels (1:1).
* This helps players correctly interpret video dimensions without SAR scaling.
*/
private fun buildPaspBox(): ByteArray {
val output = ByteArrayOutputStream()
val dos = DataOutputStream(output)
dos.writeInt(1) // hSpacing (horizontal)
dos.writeInt(1) // vSpacing (vertical)
return wrapBox("pasp", output.toByteArray())
}
private fun buildAvcCBox(sps: ByteArray, pps: ByteArray): ByteArray { private fun buildAvcCBox(sps: ByteArray, pps: ByteArray): ByteArray {
val output = ByteArrayOutputStream() val output = ByteArrayOutputStream()
val dos = DataOutputStream(output) val dos = DataOutputStream(output)
val profileIdc = if (sps.isNotEmpty()) sps[0].toInt() and 0xFF else 0x42 // SPS NAL unit format: [NAL header, profile_idc, constraint_flags, level_idc, ...]
val profileCompat = if (sps.size > 1) sps[1].toInt() and 0xFF else 0x00 // Skip byte 0 (NAL header, typically 0x67) to get the actual profile data
val levelIdc = if (sps.size > 2) sps[2].toInt() and 0xFF else 0x1F val profileIdc = if (sps.size > 1) sps[1].toInt() and 0xFF else 0x42
val profileCompat = if (sps.size > 2) sps[2].toInt() and 0xFF else 0x00
val levelIdc = if (sps.size > 3) sps[3].toInt() and 0xFF else 0x1F
dos.writeByte(1) // configuration version dos.writeByte(1) // configuration version
dos.writeByte(profileIdc) // AVC profile dos.writeByte(profileIdc) // AVC profile

View File

@@ -235,10 +235,15 @@ class PersistentCameraCaptureSession(private val cameraManager: CameraManager, p
// 1. Run a precapture sequence for AF, AE and AWB. // 1. Run a precapture sequence for AF, AE and AWB.
focusJob = coroutineScope.launch { focusJob = coroutineScope.launch {
val request = repeatingRequest.createCaptureRequest(device, deviceDetails, outputs) try {
val options = val request = repeatingRequest.createCaptureRequest(device, deviceDetails, outputs)
PrecaptureOptions(listOf(PrecaptureTrigger.AF, PrecaptureTrigger.AE), Flash.OFF, listOf(point), false, FOCUS_RESET_TIMEOUT) val options =
session.precapture(request, deviceDetails, options) PrecaptureOptions(listOf(PrecaptureTrigger.AF, PrecaptureTrigger.AE), Flash.OFF, listOf(point), false, FOCUS_RESET_TIMEOUT)
session.precapture(request, deviceDetails, options)
} catch (e: CaptureTimedOutError) {
// Focus timed out - this is non-fatal, just log and continue
Log.w(TAG, "Focus timed out at point $point, continuing without focus lock")
}
} }
focusJob?.join() focusJob?.join()

View File

@@ -198,8 +198,10 @@ class PreviewView(context: Context, callback: SurfaceHolder.Callback) :
val viewOrientation = Orientation.PORTRAIT val viewOrientation = Orientation.PORTRAIT
val rotated = point.rotatedBy(viewSize, cameraSize, viewOrientation, sensorOrientation) val rotated = point.rotatedBy(viewSize, cameraSize, viewOrientation, sensorOrientation)
Log.i(TAG, "Converted layer point $point to camera point $rotated! ($sensorOrientation, $cameraSize -> $viewSize)") // Clamp to valid camera coordinates (must be non-negative for MeteringRectangle)
return rotated val clamped = Point(maxOf(0, rotated.x), maxOf(0, rotated.y))
Log.i(TAG, "Converted layer point $point to camera point $clamped! ($sensorOrientation, $cameraSize -> $viewSize)")
return clamped
} }
private fun updateLayout() { private fun updateLayout() {