Compare commits

..

2 Commits

Author SHA1 Message Date
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 24 additions and 16 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,15 +235,10 @@ 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 {
try { val request = repeatingRequest.createCaptureRequest(device, deviceDetails, outputs)
val request = repeatingRequest.createCaptureRequest(device, deviceDetails, outputs) val options =
val options = PrecaptureOptions(listOf(PrecaptureTrigger.AF, PrecaptureTrigger.AE), Flash.OFF, listOf(point), false, FOCUS_RESET_TIMEOUT)
PrecaptureOptions(listOf(PrecaptureTrigger.AF, PrecaptureTrigger.AE), Flash.OFF, listOf(point), false, FOCUS_RESET_TIMEOUT) session.precapture(request, deviceDetails, options)
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,10 +198,8 @@ 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)
// Clamp to valid camera coordinates (must be non-negative for MeteringRectangle) Log.i(TAG, "Converted layer point $point to camera point $rotated! ($sensorOrientation, $cameraSize -> $viewSize)")
val clamped = Point(maxOf(0, rotated.x), maxOf(0, rotated.y)) return rotated
Log.i(TAG, "Converted layer point $point to camera point $clamped! ($sensorOrientation, $cameraSize -> $viewSize)")
return clamped
} }
private fun updateLayout() { private fun updateLayout() {