Repository navigation
fix: streams above 1080p in the single view: decoder set up from the SPS (#166) #220
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
7942938
fix: single view tells the decoder the largest picture size, for stre…
compot-dev 6dd18a2
chore: version 2.14
compot-dev 6053de0
Merge v2 into fix/large-frames-166
compot-dev 23e06ca
chore: version 2.16 (2.15 went to #215)
compot-dev 64a0b55
Merge remote-tracking branch 'origin/v2' into fix/large-frames-166
compot-dev 0109a57
chore: version 2.17 (2.16 went to #224)
compot-dev 0f0a9ba
Merge remote-tracking branch 'origin/v2' into fix/large-frames-166
compot-dev f736851
chore: version 2.18 (2.17 went to #225)
compot-dev 21c469a
Merge remote-tracking branch 'origin/v2' into fix/large-frames-166
compot-dev 3fed008
fix: single view sets the decoder up for the stream's size from its S…
compot-dev 35f2bc8
chore: version 2.17.1
compot-dev e31d927
Merge branch 'v2' into fix/large-frames-166
compot-dev e2bbfae
Merge branch 'v2' into fix/large-frames-166
compot-dev 4046f6b
fix: software fallback review fixes — failures, no room, size key, pa…
compot-dev 0fe8807
fix: software fallback keeps every failed size and starts at a fresh …
compot-dev File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
341 changes: 297 additions & 44 deletions
341
android/app/src/main/java/com/openipc/decoder/MediaCodecManager.java
Large diffs are not rendered by default.
Oops, something went wrong.
196 changes: 196 additions & 0 deletions
196
android/app/src/main/java/com/openipc/decoder/SpsParser.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,196 @@ | ||
| /* | ||
| * Copyright (c) OpenIPC https://openipc.org Prosperity Public License 3.0.0 | ||
| * | ||
| * SpsParser.java — picture size from an H.264 / H.265 sequence parameter set | ||
| * | ||
| */ | ||
|
|
||
| package com.openipc.decoder; | ||
|
|
||
| /** | ||
| * Reads the visible picture size (after the cropping / conformance window) from an SPS, so a | ||
| * decoder can be configured for the stream's real size before the first frame (#166). | ||
| */ | ||
| final class SpsParser { | ||
| private SpsParser() {} | ||
|
|
||
| /** | ||
| * Size {width, height} from one NAL unit (no start code, header included), or null if it | ||
| * is not an SPS or cannot be read. | ||
| */ | ||
| static int[] size(byte[] nal, int offset, int length, boolean h265) { | ||
| if (nal == null || length < (h265 ? 3 : 2) || offset < 0 | ||
| || offset + length > nal.length) return null; | ||
| try { | ||
| Bits b = new Bits(unescape(nal, offset, length)); | ||
| return h265 ? h265(b) : h264(b); | ||
| } catch (RuntimeException e) { | ||
| // Truncated or malformed: the size stays unknown | ||
| return null; | ||
| } | ||
| } | ||
|
|
||
| private static int[] h264(Bits b) { | ||
| if ((b.u(8) & 0x1f) != MediaCodecManager.H264_NAL_SPS) return null; | ||
| int profile = b.u(8); | ||
| b.u(8); // constraint flags | ||
| b.u(8); // level | ||
| b.ue(); // seq_parameter_set_id | ||
| int chroma = 1; | ||
| boolean separatePlanes = false; | ||
| if (profile == 100 || profile == 110 || profile == 122 || profile == 244 | ||
| || profile == 44 || profile == 83 || profile == 86 || profile == 118 | ||
| || profile == 128 || profile == 138 || profile == 139 || profile == 134 | ||
| || profile == 135) { | ||
| chroma = b.ue(); | ||
| if (chroma == 3) separatePlanes = b.u(1) == 1; | ||
| b.ue(); // bit_depth_luma_minus8 | ||
| b.ue(); // bit_depth_chroma_minus8 | ||
| b.u(1); // qpprime_y_zero_transform_bypass_flag | ||
| if (b.u(1) == 1) { | ||
| for (int i = 0; i < (chroma != 3 ? 8 : 12); i++) { | ||
| if (b.u(1) == 1) skipScalingList(b, i < 6 ? 16 : 64); | ||
| } | ||
| } | ||
| } | ||
| b.ue(); // log2_max_frame_num_minus4 | ||
| int pocType = b.ue(); | ||
| if (pocType == 0) { | ||
| b.ue(); // log2_max_pic_order_cnt_lsb_minus4 | ||
| } else if (pocType == 1) { | ||
| b.u(1); // delta_pic_order_always_zero_flag | ||
| b.se(); // offset_for_non_ref_pic | ||
| b.se(); // offset_for_top_to_bottom_field | ||
| int n = b.ue(); | ||
| for (int i = 0; i < n; i++) b.se(); | ||
| } | ||
| b.ue(); // max_num_ref_frames | ||
| b.u(1); // gaps_in_frame_num_value_allowed_flag | ||
| // In long: a malformed code must not wrap round to a small size (Qodo on #220) | ||
| long widthMbs = b.ueLong() + 1; | ||
| long heightUnits = b.ueLong() + 1; | ||
| int frameMbsOnly = b.u(1); | ||
| if (frameMbsOnly == 0) b.u(1); // mb_adaptive_frame_field_flag | ||
| b.u(1); // direct_8x8_inference_flag | ||
| long width = widthMbs * 16; | ||
| long height = (2 - frameMbsOnly) * heightUnits * 16; | ||
| if (b.u(1) == 1) { | ||
| long left = b.ueLong(), right = b.ueLong(), top = b.ueLong(), bottom = b.ueLong(); | ||
| int arrayType = separatePlanes ? 0 : chroma; | ||
| int cropX = arrayType == 1 || arrayType == 2 ? 2 : 1; | ||
| int cropY = (arrayType == 1 ? 2 : 1) * (2 - frameMbsOnly); | ||
| width -= cropX * (left + right); | ||
| height -= cropY * (top + bottom); | ||
| } | ||
| return valid(width, height); | ||
| } | ||
|
|
||
| private static void skipScalingList(Bits b, int size) { | ||
| int last = 8, next = 8; | ||
| for (int j = 0; j < size; j++) { | ||
| if (next != 0) next = (last + b.se() + 256) % 256; | ||
| last = next == 0 ? last : next; | ||
| } | ||
| } | ||
|
|
||
| private static int[] h265(Bits b) { | ||
| if (((b.u(8) >> 1) & 0x3f) != MediaCodecManager.H265_NAL_SPS) return null; | ||
| b.u(8); // rest of the NAL header | ||
| b.u(4); // sps_video_parameter_set_id | ||
| int subLayers = b.u(3); // sps_max_sub_layers_minus1 | ||
| b.u(1); // sps_temporal_id_nesting_flag | ||
| // profile_tier_level(1, subLayers): general part, 96 bits | ||
| b.skip(96); | ||
| boolean[] profile = new boolean[subLayers]; | ||
| boolean[] level = new boolean[subLayers]; | ||
| for (int i = 0; i < subLayers; i++) { | ||
| profile[i] = b.u(1) == 1; | ||
| level[i] = b.u(1) == 1; | ||
| } | ||
| if (subLayers > 0) b.skip(2 * (8 - subLayers)); | ||
| for (int i = 0; i < subLayers; i++) { | ||
| if (profile[i]) b.skip(88); | ||
| if (level[i]) b.skip(8); | ||
| } | ||
| b.ue(); // sps_seq_parameter_set_id | ||
| int chroma = b.ue(); | ||
| boolean separatePlanes = chroma == 3 && b.u(1) == 1; | ||
| long width = b.ueLong(); | ||
| long height = b.ueLong(); | ||
| if (b.u(1) == 1) { | ||
| long left = b.ueLong(), right = b.ueLong(), top = b.ueLong(), bottom = b.ueLong(); | ||
| int arrayType = separatePlanes ? 0 : chroma; | ||
| int subW = arrayType == 1 || arrayType == 2 ? 2 : 1; | ||
| int subH = arrayType == 1 ? 2 : 1; | ||
| width -= subW * (left + right); | ||
| height -= subH * (top + bottom); | ||
| } | ||
| return valid(width, height); | ||
| } | ||
|
|
||
| private static int[] valid(long width, long height) { | ||
| // Beyond 8K, empty or cropped below nothing: not a size worth configuring a decoder for | ||
| return width > 0 && height > 0 && width <= 8192 && height <= 8192 | ||
| ? new int[]{(int) width, (int) height} : null; | ||
| } | ||
|
|
||
| /** The NAL unit without its emulation prevention bytes (00 00 03 → 00 00). */ | ||
| static byte[] unescape(byte[] data, int offset, int length) { | ||
| byte[] out = new byte[length]; | ||
| int n = 0, zeros = 0; | ||
| for (int i = offset; i < offset + length; i++) { | ||
| int v = data[i] & 0xff; | ||
| if (zeros >= 2 && v == 3) { | ||
| zeros = 0; | ||
| continue; | ||
| } | ||
| zeros = v == 0 ? zeros + 1 : 0; | ||
| out[n++] = (byte) v; | ||
| } | ||
| return java.util.Arrays.copyOf(out, n); | ||
| } | ||
|
|
||
| /** MSB-first bit reader with Exp-Golomb codes; reading past the end throws. */ | ||
| private static final class Bits { | ||
| private final byte[] data; | ||
| private int pos; | ||
|
|
||
| Bits(byte[] data) { this.data = data; } | ||
|
|
||
| int u(int n) { | ||
| int v = 0; | ||
| for (int i = 0; i < n; i++) { | ||
| if (pos >= data.length * 8) throw new IllegalStateException("SPS truncated"); | ||
| v = (v << 1) | ((data[pos >> 3] >> (7 - (pos & 7))) & 1); | ||
| pos++; | ||
| } | ||
| return v; | ||
| } | ||
|
|
||
| void skip(int n) { | ||
| if (pos + n > data.length * 8) throw new IllegalStateException("SPS truncated"); | ||
| pos += n; | ||
| } | ||
|
|
||
| /** Unsigned Exp-Golomb code, 0 … 2^32 − 2. */ | ||
| long ueLong() { | ||
| int zeros = 0; | ||
| while (u(1) == 0) { | ||
| if (++zeros > 31) throw new IllegalStateException("bad Exp-Golomb code"); | ||
| } | ||
| return zeros == 0 ? 0 : (1L << zeros) - 1 + (u(zeros) & 0xffffffffL); | ||
| } | ||
|
|
||
| /** {@link #ueLong()} where the syntax needs an int; larger values are malformed. */ | ||
| int ue() { | ||
| long v = ueLong(); | ||
| if (v > Integer.MAX_VALUE) throw new IllegalStateException("Exp-Golomb code too large"); | ||
| return (int) v; | ||
| } | ||
|
|
||
| int se() { | ||
| int k = ue(); | ||
| return (k & 1) == 1 ? (k + 1) / 2 : -(k / 2); | ||
| } | ||
| } | ||
| } |
137 changes: 137 additions & 0 deletions
137
android/app/src/test/java/com/openipc/decoder/SpsParserTest.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,137 @@ | ||
| /* | ||
| * Copyright (c) OpenIPC https://openipc.org Prosperity Public License 3.0.0 | ||
| * | ||
| * SpsParserTest.java — picture size from H.264 / H.265 SPS (#166) | ||
| * | ||
| */ | ||
|
|
||
| package com.openipc.decoder; | ||
|
|
||
| import static org.junit.Assert.assertArrayEquals; | ||
| import static org.junit.Assert.assertNull; | ||
|
|
||
| import org.junit.Test; | ||
|
|
||
| public class SpsParserTest { | ||
| // SPS from ffmpeg 6.1 (libx264 / libx265), testsrc, emulation prevention bytes included | ||
| private static final String H264_2304 = "67640032acd9402400a3b011000003000100000300320f183196"; | ||
| private static final String H264_1080 = "674d4028eca03c0113f2e022000003000200000300641e30632c"; | ||
| private static final String H264_720 = "6742c01fd9005005bb011000000300100000030320f1832480"; | ||
| private static final String H264_444 = "67f40028919b280f0044fc4e0220000003002000000641e30632c0"; | ||
| private static final String H264_INTERLACED = | ||
| "67640028acd94078044fde0220000003002000000643e2c5b2c0"; | ||
| private static final String H265_2304 = "420101016000000300900000030000030096a0012020051165959a" | ||
| + "4932bc05a02000000300200000030321"; | ||
| private static final String H265_1080 = "420101016000000300900000030000030078a003c08010e5965669" | ||
| + "24caf0168080000003008000000c84"; | ||
|
|
||
| private static int[] size(String hex, boolean h265) { | ||
| byte[] b = hex(hex); | ||
| return SpsParser.size(b, 0, b.length, h265); | ||
| } | ||
|
|
||
| private static byte[] hex(String s) { | ||
| byte[] b = new byte[s.length() / 2]; | ||
| for (int i = 0; i < b.length; i++) { | ||
| b[i] = (byte) Integer.parseInt(s.substring(2 * i, 2 * i + 2), 16); | ||
| } | ||
| return b; | ||
| } | ||
|
|
||
| @Test | ||
| public void h264Sizes() { | ||
| // High, 2304×1296: 144×81 macroblocks, no cropping | ||
| assertArrayEquals(new int[]{2304, 1296}, size(H264_2304, false)); | ||
| // Main, 1080p: 1088 coded, cropped to 1080 | ||
| assertArrayEquals(new int[]{1920, 1080}, size(H264_1080, false)); | ||
| assertArrayEquals(new int[]{1280, 720}, size(H264_720, false)); | ||
| // High 4:4:4: crop units of one line | ||
| assertArrayEquals(new int[]{1920, 1080}, size(H264_444, false)); | ||
| // Interlaced: map units are field macroblock pairs | ||
| assertArrayEquals(new int[]{1920, 1080}, size(H264_INTERLACED, false)); | ||
| } | ||
|
|
||
| @Test | ||
| public void h265Sizes() { | ||
| assertArrayEquals(new int[]{2304, 1296}, size(H265_2304, true)); | ||
| // Coded 1920×1088 with a conformance window | ||
| assertArrayEquals(new int[]{1920, 1080}, size(H265_1080, true)); | ||
| } | ||
|
|
||
| @Test | ||
| public void notAnSpsOrBroken() { | ||
| // PPS | ||
| assertNull(size("68ebe3cb22c0", false)); | ||
| // H.265 VPS | ||
| assertNull(size("40010c01ffff016000000300900000030000030078959809", true)); | ||
| // Truncated | ||
| assertNull(size(H264_2304.substring(0, 16), false)); | ||
| assertNull(size(H265_2304.substring(0, 40), true)); | ||
| // Codec mixed up | ||
| assertNull(size(H264_1080, true)); | ||
| assertNull(SpsParser.size(null, 0, 0, false)); | ||
| byte[] b = hex(H264_720); | ||
| assertNull(SpsParser.size(b, 4, b.length, false)); | ||
| } | ||
|
|
||
| /** A baseline H.264 SPS with the given size fields (Exp-Golomb values as written). */ | ||
| private static int[] h264Sps(long widthMbsMinus1, long heightMinus1, long[] crop) { | ||
| StringBuilder bits = new StringBuilder(); | ||
| bits.append(bin(0x67, 8)).append(bin(66, 8)).append(bin(0, 8)).append(bin(31, 8)); | ||
| ue(bits, 0); // seq_parameter_set_id | ||
| ue(bits, 0); // log2_max_frame_num_minus4 | ||
| ue(bits, 2); // pic_order_cnt_type | ||
| ue(bits, 1); // max_num_ref_frames | ||
| bits.append('0'); // gaps_in_frame_num_value_allowed_flag | ||
| ue(bits, widthMbsMinus1); | ||
| ue(bits, heightMinus1); | ||
| bits.append("11"); // frame_mbs_only_flag, direct_8x8_inference_flag | ||
| if (crop != null) { | ||
| bits.append('1'); | ||
| for (long c : crop) ue(bits, c); | ||
| } else { | ||
| bits.append('0'); | ||
| } | ||
| bits.append("01"); // no VUI, then the stop bit | ||
| while (bits.length() % 8 != 0) bits.append('0'); | ||
| byte[] b = new byte[bits.length() / 8]; | ||
| for (int i = 0; i < b.length; i++) { | ||
| b[i] = (byte) Integer.parseInt(bits.substring(8 * i, 8 * i + 8), 2); | ||
| } | ||
| return SpsParser.size(b, 0, b.length, false); | ||
| } | ||
|
|
||
| private static String bin(long v, int n) { | ||
| StringBuilder s = new StringBuilder(Long.toBinaryString(v)); | ||
| while (s.length() < n) s.insert(0, '0'); | ||
| return s.toString(); | ||
| } | ||
|
|
||
| private static void ue(StringBuilder bits, long v) { | ||
| String code = Long.toBinaryString(v + 1); | ||
| for (int i = 1; i < code.length(); i++) bits.append('0'); | ||
| bits.append(code); | ||
| } | ||
|
|
||
| @Test | ||
| public void malformedSizesDoNotWrapRound() { | ||
| // The writer itself: 1280×720 | ||
| assertArrayEquals(new int[]{1280, 720}, h264Sps(79, 44, null)); | ||
| // 2^28 + 1 macroblocks wide is 2^32 + 16 pixels: 16 in 32-bit arithmetic (Qodo on #220) | ||
| assertNull(h264Sps(1L << 28, 44, null)); | ||
| // Crops larger than the picture, and one that overflows an int when doubled | ||
| assertNull(h264Sps(79, 44, new long[]{700, 0, 0, 0})); | ||
| assertNull(h264Sps(79, 44, new long[]{1L << 31, 0, 0, 0})); | ||
| // A sane crop still works: 1920×1088 coded, 1080 shown | ||
| assertArrayEquals(new int[]{1920, 1080}, h264Sps(119, 67, new long[]{0, 0, 0, 4})); | ||
| } | ||
|
|
||
| @Test | ||
| public void emulationPreventionBytesAreRemoved() { | ||
| assertArrayEquals(new byte[]{0, 0, 1, 0, 0, 0}, | ||
| SpsParser.unescape(new byte[]{0, 0, 3, 1, 0, 0, 3, 0}, 0, 8)); | ||
| // 00 00 03 03: only the first 03 goes | ||
| assertArrayEquals(new byte[]{0, 0, 3}, | ||
| SpsParser.unescape(new byte[]{0, 0, 3, 3}, 0, 4)); | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.