mediadecode-ffmpeg 0.9.0

FFmpeg adapter for the `mediadecode` abstraction layer — implements its `VideoAdapter` / `AudioAdapter` / `SubtitleAdapter` traits and the matching push-style decoder traits, with hardware-acceleration auto-probe across VideoToolbox / VAAPI / NVDEC / D3D11VA and software fallback via ffmpeg-next.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
2353
2354
2355
2356
2357
2358
2359
2360
2361
2362
2363
2364
2365
2366
2367
2368
2369
2370
2371
2372
2373
2374
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
2399
2400
2401
2402
2403
2404
2405
2406
2407
2408
2409
2410
2411
2412
2413
2414
2415
2416
2417
2418
2419
2420
2421
2422
2423
2424
2425
2426
2427
2428
2429
2430
2431
2432
2433
2434
2435
2436
2437
2438
2439
2440
2441
2442
2443
2444
2445
2446
2447
2448
2449
2450
2451
2452
2453
2454
2455
2456
2457
2458
2459
2460
2461
2462
2463
2464
2465
2466
2467
2468
2469
2470
2471
2472
2473
2474
2475
2476
2477
2478
2479
2480
2481
2482
2483
2484
2485
2486
2487
2488
2489
2490
2491
2492
2493
2494
2495
2496
2497
2498
2499
2500
2501
2502
2503
2504
2505
2506
2507
2508
2509
2510
2511
2512
2513
2514
2515
2516
2517
2518
2519
2520
2521
2522
2523
2524
2525
2526
2527
2528
2529
2530
2531
2532
2533
2534
2535
2536
2537
2538
2539
2540
2541
2542
2543
2544
2545
2546
2547
2548
2549
2550
2551
2552
2553
2554
2555
2556
2557
2558
2559
2560
2561
2562
2563
2564
2565
2566
2567
2568
2569
2570
2571
2572
2573
2574
2575
2576
2577
2578
2579
2580
2581
2582
2583
2584
2585
2586
2587
2588
2589
2590
2591
2592
2593
2594
2595
2596
2597
2598
2599
2600
2601
2602
2603
2604
2605
2606
2607
2608
2609
2610
2611
2612
2613
2614
2615
2616
2617
2618
2619
2620
2621
2622
2623
2624
2625
2626
2627
2628
2629
2630
2631
2632
2633
2634
2635
2636
2637
2638
2639
2640
2641
2642
2643
2644
2645
2646
2647
2648
2649
2650
2651
2652
2653
2654
2655
2656
2657
2658
2659
2660
2661
2662
2663
2664
2665
2666
2667
2668
2669
2670
2671
2672
2673
2674
2675
2676
2677
2678
2679
2680
2681
2682
2683
2684
2685
2686
2687
2688
2689
2690
2691
2692
2693
2694
2695
2696
2697
2698
2699
2700
2701
2702
2703
2704
2705
2706
2707
2708
2709
2710
2711
2712
2713
2714
2715
2716
2717
2718
2719
2720
2721
2722
2723
2724
2725
2726
2727
2728
2729
2730
2731
2732
2733
2734
2735
2736
2737
2738
2739
2740
2741
2742
2743
2744
2745
2746
2747
2748
2749
2750
2751
2752
2753
2754
2755
2756
2757
2758
2759
2760
2761
2762
2763
2764
2765
2766
2767
2768
2769
2770
2771
2772
2773
2774
2775
2776
2777
2778
2779
2780
2781
2782
2783
2784
2785
2786
2787
2788
2789
2790
2791
2792
2793
2794
2795
2796
2797
2798
2799
2800
2801
2802
2803
2804
2805
2806
2807
2808
2809
2810
2811
2812
2813
2814
2815
2816
2817
2818
2819
2820
2821
2822
2823
2824
2825
2826
2827
2828
2829
2830
2831
2832
2833
2834
2835
2836
2837
2838
2839
2840
2841
2842
2843
2844
2845
2846
2847
2848
2849
2850
2851
2852
2853
2854
2855
2856
2857
2858
2859
2860
2861
2862
2863
2864
2865
2866
2867
2868
2869
2870
2871
2872
2873
2874
2875
2876
2877
2878
2879
2880
2881
2882
2883
2884
2885
2886
2887
2888
2889
2890
2891
2892
2893
2894
2895
2896
2897
2898
2899
2900
2901
2902
2903
2904
2905
2906
2907
2908
2909
2910
2911
2912
2913
2914
2915
2916
2917
2918
2919
2920
2921
2922
2923
2924
2925
2926
2927
2928
2929
2930
2931
2932
2933
2934
2935
2936
2937
2938
2939
2940
2941
2942
2943
2944
2945
2946
2947
2948
2949
2950
2951
2952
2953
2954
2955
2956
2957
2958
2959
2960
2961
2962
2963
2964
2965
2966
2967
2968
2969
2970
2971
2972
2973
2974
2975
2976
2977
2978
2979
2980
2981
2982
2983
2984
2985
2986
2987
2988
2989
2990
2991
2992
2993
2994
2995
2996
2997
2998
2999
3000
3001
3002
3003
3004
3005
3006
3007
3008
3009
3010
3011
3012
3013
3014
3015
3016
3017
3018
3019
3020
3021
3022
3023
3024
3025
3026
3027
3028
3029
3030
3031
3032
3033
3034
3035
3036
3037
3038
3039
3040
3041
3042
3043
3044
3045
3046
3047
3048
3049
3050
3051
3052
3053
3054
3055
3056
3057
3058
3059
3060
3061
3062
3063
3064
3065
3066
3067
3068
3069
3070
3071
3072
3073
3074
3075
3076
3077
3078
3079
3080
3081
3082
3083
3084
# Changelog

All notable changes to the [`mediadecode-ffmpeg`](https://crates.io/crates/mediadecode-ffmpeg)
crate are documented in this file.

The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this crate adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

The backend-agnostic core it adapts has its own log at
[`mediadecode/CHANGELOG.md`](../mediadecode/CHANGELOG.md).

## [Unreleased]

## [0.9.0] - 2026-08-26

### Fixed

- **The cold software fallback still lost allocator refusals.**
  `degrade_to_sw_inner` opens a *temporary* software decoder, forwards
  the failure arm's packet or EOF into it, and drops it on any error.
  That decoder owns the callback state, so a `judge_buffer` refusal
  recorded during either forward died with it — and the caller received
  an ambiguous FFmpeg error inside a `FallbackFailed` envelope. It was
  the last software road still wrapping libavcodec's `EINVAL` raw.

  The state is captured before the forward, and both calls route
  through `software_exit`.

- **And the wrapping was renamed, because the envelope was the wrong
  one.** A budget refusal during fallback now travels **unwrapped**, as
  `FrameBudgetExceeded`, instead of arriving as
  `FallbackFailed(FrameBudgetExceeded)`:

  - `FallbackFailed` means the fallback *machinery* could not complete,
    and its contract is to hand back the unconsumed packets so a caller
    can re-drive them. On the post-commit road that set is empty by
    construction — the probe buffer is gone and no replay frames are
    retained — so the envelope carries no recovery affordance, only a
    label.
  - And the label invites the wrong action. Re-driving is the natural
    response to a fallback failure, and re-driving a budget refusal
    under the same limits refuses identically. `FrameBudgetExceeded`
    names the action that can succeed: raise the ceiling, or accept the
    refusal.

  Genuine machinery failures keep the envelope. One fact, one name.

- **The hardware-pool fallbacks underpriced, in both directions.** When
  `avcodec_get_hw_frames_parameters` could not answer, the judge fell
  back to codec-aligned dimensions — a fallback whose own comment
  admitted it could be *smaller* than the pool, since D3D11 HEVC and
  AV1 round both dimensions to 128 while the codec may round to less.
  And when the pool declared dimensions in a layout libavutil would not
  size, the stand-in was a bare `w * h * 16`, omitting the dimension
  alignment and per-plane slack every accurate estimate carries.

  Both are closed. A pool that will not declare its dimensions and
  format is now **refused** — the rule the transfer judge already kept:
  an unprovable extent is not a small one, and declining the hardware
  format falls the decode back to software where the same budget
  applies. A declared-but-unpriceable layout is charged
  `footprint::video_frame_bytes_upper_bound`, built from the footprint's
  own machinery so it dominates every layout the build could have
  priced at that extent — and its per-pixel rate comes from the live
  census, not a literal, so a build with a wider format is charged for
  it.

  A context with no hardware device attached is passed through rather
  than refused: that is *no pool*, not a pool declining to describe
  itself, and this crate's hardware road always attaches a device before
  opening.

- **The transfer judge ignored unpriceable candidates in mixed lists.**
  FFmpeg picks the destination format from
  `av_hwframe_transfer_get_formats`; this crate does not. The fold
  updated its maximum only on priceable members and reached for a
  fallback only when *nothing* priced — so a list holding one cheap
  priceable format beside one unpriceable format was judged at the cheap
  price, while FFmpeg stayed free to select the member that had been
  skipped. Every candidate is folded in now, at the conservative bound
  when it cannot be priced, with an immediate refusal if even that
  cannot be formed.

- **The software-video road never consumed `FrameBudgetExceeded`.**
  `SwDecoder` owned the callback state but only `Deref`d the decoder
  underneath, so every call site kept wrapping libavcodec's error raw —
  ordinary send, receive and EOF, the replay drain, and the
  cold-fallback forward, the last of which drops the state when it
  finishes. A budget refusal on that whole road surfaced as the bare
  `EINVAL` libavcodec also uses for corrupt input.

  `SwDecoder` exposes its state now, and all nine decode-failure sites
  route through `software_exit` before wrapping. The three remaining
  `Error::Ffmpeg` constructions on that road are caps this crate mints
  itself — a replay-queue `ENOMEM`, two retry-exhaustion `EAGAIN`s — and
  have no libavcodec verdict to reinterpret.

- **The byte-derived coarse gates over-refused cheap formats.**
  `max_pixels` was written as `min(the caller's pixel limit,
  max_frame_bytes / worst-bytes-per-pixel)` and `max_samples` as
  `max_frame_bytes / worst-bytes-per-sample`, so that the byte ceiling
  could bite before libavcodec allocated. Both translations charged
  every stream the widest format in existence, and both refused
  ordinary media: a 1920x1080 `yuv420p` frame costs 3.14 MiB and was
  refused under a 4 MiB budget at `ff_set_dimensions`; a 6-channel
  `s16` frame fitting 64 KiB was refused by a ruler pricing it at `f64`
  rates.

  Both are gone. `max_pixels` carries the caller's number verbatim, and
  `max_samples` is left alone. Nothing is traded away, because the seat
  that enforces bytes — `judge_buffer` — is *itself* pre-allocation:
  `get_buffer2` **is** the allocation, and it prices the frame's real
  format at its real dimensions through the allocator-parity footprint.
  An exact judge at the allocation beats an approximate one before it.

- **The hardware pool judge carried its own bound, as it had to.** It
  compared the pool's pixel count against `AVCodecContext.max_pixels`,
  which only worked while that field held a byte-derived number. It now
  prices the pool's declared dimensions — and its `sw_format`, read as
  the integer it is — through the same footprint, against the
  `max_frame_bytes` its callback state already holds, falling back to
  the worst per-pixel rate where a layout cannot be priced.

  That is strictly more accurate than the scalar it replaces, and the
  cropped-h264 lane shows it: a 1920x1088 NV12 pool costs 3,135,488
  bytes and was refused under a 16 MiB budget it fits five times over.
  The lane now refuses it at 1 MiB, where it genuinely does not fit,
  and asserts that 16 MiB decodes.

- **Software byte refusals surfaced as bare `EINVAL`.** A `get_buffer2`
  callback can only answer libavcodec with an errno, and `EINVAL` is
  also what libavcodec reports for corrupt input — so a caller could not
  tell a budget refusal this crate made from a broken file, and only one
  of those is worth retrying with a larger ceiling.

  The refusal is recorded in the callback state, clear-on-read, and
  collected by a `software_exit` funnel that every software decoder road
  routes through — video, audio, still and subtitle. It surfaces as
  `Error::FrameBudgetExceeded`, carrying the bytes, the limit and
  whether the frame was a picture or audio.

- **The allocator judge restated a logical limit as an arithmetic one.**
  It compared the **aligned** dimensions against `AVCodecContext.max_pixels`,
  which is `min(the caller's pixel limit, byte ceiling /
  worst-bytes-per-pixel)` — so when the pixel limit was the tighter
  seat, alignment inflation alone refused frames that satisfied *both*
  requested limits. A 65536x1 `gray8` frame under `max_pixels = 65536`
  meets the pixel limit exactly on its raw dimensions and costs about
  2 MiB; against a generous byte budget it was refused anyway, for
  arithmetic the caller never asked about.

  That comparison was R11's instrument against degenerate-shape bypass,
  added when the callback had no accurate byte check and a 65536x1 frame
  could slip thirty-two times its raw pixel count past a raw gate. Once
  the caller's `max_frame_bytes` was threaded into the callback, the
  footprint — which prices the aligned dimensions itself — subsumed it.
  So the comparison is **removed**: it was redundant against the shape
  it was built for, and wrong against every frame whose pixel limit was
  the tighter of the two.

  The seats now ask one question each. **Logical extent** is
  `max_pixels`, enforced by libavcodec in `ff_set_dimensions` against
  the **raw** dimensions — the semantics that field has and that FFmpeg
  documents. **Allocation cost** is the footprint against
  `max_frame_bytes`. The `get_format` hardware judge is untouched: it is
  a separate seat, judging the pool's own declared extent.

- **The probe meter consumed bytes it then refused to hand over.** It
  read libavformat's whole request and only afterwards discovered the
  budget was exceeded, so a 32 KiB request against a 1 KiB allowance
  took all 32 KiB from the reader, returned none of them, and reported
  having read bytes nobody received. A container that would have
  finished probing inside its allowance was refused for the shape of
  libavformat's buffer rather than for its own size.

  Requests are capped to the remaining allowance now — a short read is
  ordinary, and every `Read` may return fewer bytes than asked — and
  only bytes actually delivered are charged, so
  `ProbeBudgetExhausted::read` reports what libavformat really got.

  The boundary is defined rather than incidental: a read landing
  **exactly** on zero remaining is served in full and does not trip,
  because a budget is a ceiling on what is spent and not on asking; the
  **next** nonempty read is the one refused. A zero-length request asks
  for nothing and is passed through whatever the budget says.

- **The allocator judge invented a smaller ceiling when the pixel seat
  was the tighter one.** It recovered a byte ceiling from
  `AVCodecContext.max_pixels`, which is `min(pixel ceiling, byte ceiling
  / worst-bytes-per-pixel)` — so when the pixel seat won, `max_pixels`
  no longer encoded the byte ceiling at all. A 256x256 frame at 16 bytes
  a pixel under `max_pixels = 65536` with a 2 MiB byte budget satisfies
  **both** of the caller's limits, costs 1,050,624 bytes, and was judged
  against 1,048,576 and refused — by exactly the 2,048 bytes of
  alignment and slack the recovery could not see. The claim that the
  conflation was harmless in one direction is withdrawn; it was wrong.

  `CallbackState` carries the caller's `max_frame_bytes` verbatim now.
  That seat already existed for the `get_format` declination, and
  `build_codec_context` — the single point every decoder in the crate is
  built through — allocates and installs it for every road, so the five
  callers and the software-decoder variant each keep the box alive
  beside their context. Both media read that one number; the audio
  road's `max_samples` recovery, though exact, is gone, because two
  sources of truth for one ceiling is how the first one went wrong.

### Added

- **Tier two of the [resource governance contract][gov] enumerated** in
  the `buffer` module's accounting, beside the tier-one table it already
  kept (user ruling of 2026-08-25). Every FFmpeg resource knob this
  crate sets, and what each one bounds:

  | knob | where | bounds |
  |---|---|---|
  | `AVCodecContext.max_pixels` | every opened decoder | the coded extent, in `ff_set_dimensions`, before any frame or pool exists |
  | the `get_format` coded-dims judge | the hardware road | the pool's own declared extent, from `avcodec_get_hw_frames_parameters` |
  | `AVCodecContext.max_samples` | every opened decoder | `nb_samples * channels`, in `ff_get_buffer` |
  | the `get_buffer2` byte judge | every software decode | what the allocator will actually take, pictures and audio alike |
  | the pre-transfer judge | every `av_hwframe_transfer_data` | the CPU destination, at the frames-context pool dims |
  | `probesize` / `formatprobesize` | both demux entrypoints | what probe and analysis may consume |
  | `max_streams` | both demux entrypoints | the `AVStream` array a header can conjure |
  | the `AVIOContext` byte meter | the reader demux entrypoint | total bytes libavformat is handed, hard |

  Recorded as **defense in depth, not a proof**: together they cover
  every interposition point FFmpeg exposes, which is not the same as
  covering FFmpeg. The demux residual — libavformat builds the attached
  picture, extradata and coded side data itself, so those seats measure
  this crate's copies of allocations that already happened — is folded
  into that section rather than stated twice, and the part that cannot
  be reached (a parser's amplification, and the path entrypoint's want
  of a meter) is named as tier three with a pointer to the contract.

  [gov]: https://docs.rs/mediadecode/latest/mediadecode/adapter/index.html


## [0.9.0] - 2026-08-24

### Added — the second carrier lane

*User-ruled 2026-08-25, and folded into 0.9.0 because the release is
unpublished: the bare names change meaning, and nothing outside this
workspace has seen them yet.*

- **`FfmpegBuffer` returns**, with 0.8's name and semantics: an
  `av_buffer_ref`-refcounted view onto FFmpeg's own allocation, `Send`
  and **not** `Sync`, `Clone` a refcount bump, `AsRef<[u8]>`. It is not
  0.8 restored unchanged — every lesson from the amputation round that
  lands on this type has been re-applied:

  - the **extent is proved before a view exists**. 0.8 formed a view
    over a packet's claimed range and let a malformed `size` hand out a
    slice nobody had checked; the proof now runs in `payload_of`, which
    **both lanes share**, so the view lane cannot regress it
    independently;
  - `AV_PKT_FLAG_TRUSTED` is refused on both legs, and for a sharper
    reason here — sharing an allocation does not make its pointers own
    what they name;
  - the budgets are unchanged, because they judge sizes and not copies:
    a ceiling bounds what a caller is handed and asked to hold, and on
    this lane also how long a pool slot stays out.

- **A sealed carrier seam, whose operations are not reachable at all.**
  `FfmpegCarrier` names a lane and says what it carries; every operation
  lives on `CarrierOps`, a crate-private trait that is deliberately
  **not** a supertrait of it.

  Two walls were tried before that one held. Sealing closed the seam to
  outside *implementations* and left every operation callable. Moving
  the operations onto the seal — a `pub trait` in a private module —
  looked airtight and was not: associated items resolve through a
  **bound**, not through a path, so `fn f<C: FfmpegCarrier>()` written
  in a downstream crate type-checked `C::from_rows(1, 64, |_| &[0])`
  (safe, and an out-of-bounds read) and `C::capture_packet_payload(..)`
  (which mints the padding claim the send leg trusts). Only a trait no
  writable bound reaches actually stops a call. Six `compile_fail`
  doctests — rustdoc compiles each as a separate downstream crate — pin
  that, each against the exact error code.

  In the same spirit, `FfmpegBuffer`'s extent-taking constructors are
  crate-private, and the row gather **checks** each row's length rather
  than asserting it: a `debug_assert` is a note to the author, not a
  bounds check, and it is absent from the profile that matters.

  **The public faces are written per lane** so that no signature a
  consumer reads names a trait they cannot: `CarrierDemuxer`, the four
  decoders and `CarrierResampler` carry only `C: FfmpegCarrier` on their
  declarations, and their inherent and trait impls are macro-generated
  once per lane over crate-private generic bodies. A consumer can
  therefore hold and name `CarrierDemuxer<C>` in their own generic code
  and be generic over `C::Buffer`. What they cannot do is *drive* a lane
  generically — that needs the operations — so lane-generic helpers take
  `C::Buffer` and the doors are instantiated at the two concrete lanes.
  Compile-**pass** doctests pin the first, the six compile-fail ones pin
  the second.

- **A refusal that another attempt could survive costs no packet.**
  `av_read_frame` advances the container: once it returns, that packet
  is off the wire and nothing brings it back. A conversion that then
  failed on an *allocation* — a refcount the view lane could not take,
  a copy the shared road could not make — dropped it, leaving a live
  session that answered the next pull with the **following** packet.
  Compressed data and subtitle cues went missing under memory pressure,
  quietly, and a caller who retried could not tell.

  The read and the conversion are now one transaction with a seat
  between them: a transient refusal parks the packet, and the next pull
  re-attempts *that* packet before reading another. It is the same
  park-then-replay the decode household already runs — the video
  decoder holds `sw_replay_frames`, the probe holds its rescue history —
  for the same reason: a byte C has already given up is not re-askable.
  The provenance is parked with the packet rather than re-probed, since
  it is an observation about the moment of delivery.

  Only *transient* refusals park, decided by a crate-private
  `parks_in_demux`: an allocation
  that failed says nothing about the packet, while a payload outside its
  own buffer or a body past the ceiling is a fact about it, and parking
  those would answer every later pull with the same error instead of
  letting the session make progress.

  That predicate was briefly public as `is_transient` and should not
  have been: whether retrying helps depends on *what was retried*.
  `SharedPayload` is permanent for a caller who keeps their other
  reference and retryable the moment they drop it; `CaptureFailed` is
  worth another attempt only if the packet still exists to attempt,
  which on a consuming conversion it does not. Only the demux loop knows
  both halves. A public retry signal would have to be operation-aware —
  a door left open, not a thing approximated now.

  The arms, censused: the four timed roads need the seat and have it;
  the duplicate-attachment road converts nothing, so there is nothing to
  park; and the hoisted attachment happens during `open`, where a
  failure fails the open and no session exists to lose anything. Two
  fault lanes drive it under a capped FFmpeg allocator — one on the
  unique road where `av_buffer_ref` fails, one on the shared road where
  the copy fails — each asserting that the retry re-attempts the same
  packet and that the recovered stream equals the reference exactly.

  **The seat is cleared by a seek that happened, not by one that was
  attempted.** A successful seek discards the parked packet, which
  belongs to the position being left. A *failed* seek does not: the
  session stays where it was, and that packet is off the wire, so
  discarding it would be the same silent loss with nothing able to
  re-read it. FFmpeg does not specify where a container sits after a
  seek that errored, and this crate does not guess — it keeps a packet
  the container really did deliver, and a caller who saw the seek fail
  knows the position is not the one they asked for.

- **Decoded frames get the same transaction.** `receive_frame` advances
  libavcodec exactly as `av_read_frame` advances a container: the frame
  it fills the scratch with is out of the codec's queue and nothing
  re-offers it. A conversion that then failed on an allocation left it
  in a scratch the next call overwrote. Both timed decoders now hold the
  scratch until a carrier exists for it, and the video decoder's replay
  queue is **peeked, not popped** — that queue is the rescue history's
  only copy, so popping before the conversion committed lost the very
  frames it exists to preserve.

  Every receive road, censused:

  | road | seat | why |
  |---|---|---|
  | audio | yes | scratch held until the conversion commits |
  | video, direct (HW and SW) | yes | same, on whichever scratch the state uses — **and the sends refuse while it is taken**, see below |
  | video, replay queue (both entries) | yes | peeked; popped only on success |
  | subtitle | yes | the decoded `AVSubtitle` is held and the conversion moved to `receive_frame` |
  | image, one-shot | **not needed** | the packet is *borrowed*, so a failure leaves the caller everything needed to call again — and the decoder is now flushed on that road so the retry starts where the first attempt did |
  | resampler output | **not needed** | its queue holds frames already built: the carrier's claim is taken in `prepare_output`, before `swr` consumes anything, and `finish_output` is infallible by construction. That is what the reserve-then-commit seam was for |

  `ConvertError` gained the split this needed: `BufferAcquireFailed` was
  one arm for two facts — a plane outside the frame's own buffers, which
  is permanent, and an allocation that failed, which is not. The second
  is now `CarrierAllocFailed`, and only it parks; the subtitle bitmap's
  data copy was reclassified onto it too. Fault lanes on audio, video,
  the replay queue and subtitle drive a capped allocator at the moment
  of conversion and assert the retry yields the *same* output.

  The replay queue has **two** delivery entries, not one: the peek at
  the top of `receive_frame`, and the branch that runs when the probe is
  exhausted at *frame* time, where `fall_back_to_sw` fills the queue and
  the head is converted in that same call. The second popped before
  converting. Both peek now, and every access to that queue is
  accounted for — two entries, one `append`, one `clear`, one test
  predicate.

  **A parked video frame pins the state that parked it.** That decoder
  has two scratches and can change which one is current, so a seat that
  merely recorded "something is parked" was not enough: a
  hardware-to-software fallback committed while a hardware frame was
  parked sent the retry to the *software* scratch — a stale frame, or a
  permanent refusal that stranded a decoded one. Both send roads can
  commit that fallback, so both now refuse by name
  (`VideoDecodeError::FramePending`) while the seat is taken.

  Two shapes were weighed. Recording the producing road in the seat is
  more permissive but adds state that every future delivery road must
  remember to keep in step — which is precisely what had just gone
  wrong. Refusing instead makes the retry's state the state that parked
  it **by construction**, is smaller, and is the discipline this crate
  already keeps one seat over (`SubtitleDecodeError::FramePending`). The
  deadlock census came out clean: while parked, `receive_frame` is
  always callable and always makes progress or repeats the same
  parkable refusal, and a caller who would rather abandon the frame
  calls `flush` — the same two answers the subtitle seat has always
  given. A caller who ignores the refusal and pushes on gets named
  errors rather than silent loss, which is the contract, not a
  regression of it.

  And every delivery now passes one **commit point**. The bookkeeping a
  delivery owes — clearing the seat, and clearing a keyframe-anchored
  resync guard — was attached to some roads and not others, so a parked
  software recovery frame delivered on the retry road skipped
  `resync_on_frame` and let a clean EOF escalate with a false
  `PostCommitNeverResynced`. All five roads (both scratches, both replay
  entries, the retry) funnel through `commit_delivery`, and a regression
  drives a parked post-keyframe recovery frame to a clean EOF.

  The subtitle road needed the seat most of all, and had the least: its
  conversion ran inside `send_packet`, so an allocation that failed
  freed the scratch and returned — and the cue, which
  `avcodec_decode_subtitle2` had already consumed the packet to make,
  was simply gone. The decoded `AVSubtitle` is now held until
  `receive_frame` has a carrier for it. Unlike the image road there is
  no resubmit to fall back on: the caller's packet is spent.

- **A view is never taken from a buffer somebody else still references.**
  Consuming the source proves no other handle survives only if the
  source's buffer was the caller's alone to give — and
  `ffmpeg_next::Packet::clone` calls `av_packet_ref` followed by
  `av_packet_make_writable` and **ignores both return codes**, so under
  allocation failure it returns a "clone" that silently still shares.
  Feeding one to a consuming conversion produced a view of a buffer the
  caller kept, and could still write through.

  The payload road now asks the buffer directly, before any byte of it
  is read, and **refuses by name**`PacketBufferError::SharedPayload`,
  carrying the count — when the answer is anything but one.

  It first answered with a silent copy, on the argument that a copy is
  always sound and keeps the API total. **That was overturned, and the
  correction is worth stating**: a copy needs a *read*, and a refcount
  above one is exactly the state in which another safe `data_mut`
  handle may exist and may be writing — on another thread, since
  `Packet` is `Send`. The read *is* the race. Totality was traded for
  soundness the wrong way round; the refusal now happens with the
  ordering visible in one function, nothing between the bounds proof and
  the refusal touching the payload.

  The rule is about **who** holds the other reference, not the count.
  `PayloadProvenance` distinguishes a delivered packet — where the
  second reference is another `Packet` and `data_mut` is one call away —
  from a container's `attached_pic`, which libavformat holds for the
  lifetime of the format context and never writes again. A blanket
  "refcount must be one" looked right and refused every embedded cover
  picture in the corpus; the distinction is now a parameter with its
  reason attached, and a test that fails without it.

  The dichotomy that survived is **delivered by libavformat** versus
  **handed over by a caller**, and it took two counterexamples to find.
  A parked cover picture is one. The other is a whole family: every
  demuxer built on `FFDemuxSubtitlesQueue` — SubRip, SubViewer,
  MicroDVD, WebVTT — parses its cues at open, keeps the packets, and
  answers each read with an `av_packet_ref` of one, so **every** packet
  it delivers has two references. A rule demanding a lone reference
  refused all of them, on both lanes.

  What separates the two sides is who can *write*, not how many
  references exist. Every secondary reference to a demux-delivered
  packet is libavformat's own: no `ffmpeg_next::Packet` wraps one, so
  there is no safe `data_mut` to race, and while this crate reads it
  holds the `AVFormatContext` exclusively. A caller's packet may share
  with another `Packet`, whose `data_mut` writes in place from safe code
  without consulting writability — that is the writer the rule exists
  for.

  Three rows, each with its argument recorded next to it:

  | provenance | unique | shared | why |
  |---|---|---|---|
  | caller-supplied | capture | **refuse** | a second `Packet`'s `data_mut` writes in place, possibly on another thread |
  | demux-delivered | capture | **copy** | the read is race-free, but a *window* would outlive the exclusivity that makes it so |
  | attached picture | capture | **capture** | written once while the container opened and never again, so a window is as stable as one onto a private buffer |

  The middle row is the deliberate one. Sharing there would have rested
  on "libavformat honours its own copy-on-write rules for as long as the
  carrier lives"; copying rests on "nothing can be writing while we hold
  the context", which is a fact about the call rather than a promise
  about anyone's future behaviour. Subtitle cues are measured in bytes,
  so the copy is not a cost worth an argument.

  The attached-picture row reaches one road more than the hoist:
  libavformat queues a stream's parked picture as that stream's **first
  packet**, and a stream carrying `ATTACHED_PIC | TIMED_THUMBNAILS`  chapter thumbnails — is deliberately classified here as **video**, so
  its first pull arrives through the ordinary read path.

  The identity is a **proof, not a heuristic**: `av_buffer_ref` copies
  the source's `buffer` field, so two `AVBufferRef`s name one allocation
  exactly when their `buffer` pointers match — comparing the references
  themselves would answer "no" to the very case this is for. Disposition
  bits say a stream *has* a parked picture, not that this packet is it,
  and "the first packet" is an ordering assumption libavformat's
  contract does not fix; neither is used.

  Two fault lanes prove the refusal: a deterministic shared packet built
  with `av_packet_ref`, and a subprocess lane that caps FFmpeg's
  allocator to reproduce the silently-shared clone itself.

  The lesson, recorded because it cost a round: **a dependency's happy
  path is not its contract.** The verification that missed this read
  `Clone` and confirmed it deep-copies. It did not read what happens
  when the copy fails.

- **A submission that can be recorded is not shared.** The scoped
  submission's proof — built, lent, dropped, so nothing that could take
  a `&mut` into the shared bytes ever exists — is a claim about the
  function, and the hardware probe made it false. While probing, the
  video decoder `av_packet_ref`s every accepted packet into a rescue
  history so a caller can replay it after a failed fallback, and
  `FallbackFailed::unconsumed_packets` hands those recordings back as
  owned, **mutable** `Packet`s: a live mutable alias of a carrier the
  caller may still be reading.

  The route is now chosen per submission. Inside the recording window
  the body is copied; once the probe commits — and on every software
  road, which records nothing — the send is zero-copy as before. The
  regression drives a probe-era failure on the view lane with every
  carrier retained, and checks that no rescued packet addresses any of
  them and that writing through all of them changes none. Flipping the
  route back to sharing makes it fail, which is how the lane was shown
  to be live.

- **The default is the view lane.** `FfmpegDemuxer` and the bare alias
  family (`VideoPacket`, `AudioPacket`, `SubtitlePacket`, `DataPacket`,
  `AttachmentPacket`, `DemuxedPacket`) now mean **View**; the owned
  family is explicit — `FfmpegOwnedDemuxer`, `OwnedVideoPacket`, and so
  on. Both aliases point at one generic `CarrierDemuxer<C>` with one set
  of constructors, so `FfmpegDemuxer::open` and
  `FfmpegOwnedDemuxer::open` both resolve and read the same.

  **This is a semantic shift in the bare names**, recorded here rather
  than in a migration note: what `VideoPacket` denotes changed from a
  copied payload to a viewed one. Nothing outside this workspace can
  have depended on it.

- **The send leg shares too, where it can prove it may — and only
  inside a scoped submission.** A packet a caller is *handed* is an
  `ffmpeg_next::Packet`, which lends `&mut [u8]` through `data_mut`
  while the carrier it was built from still lends `&[u8]`: a shared body
  there is an aliasing `&mut` out of entirely safe code, and `!Sync`
  does not help because one thread suffices. So the public reverse
  builders copy on **both** lanes, and the zero-copy send lives on a
  crate-private road that builds the packet, submits it to the decoder,
  and drops it inside one call — no value that could produce a `&mut`
  into the shared bytes ever exists.

  Sharing there needs two facts, not one. **Provenance**: only a payload
  captured out of an `AVPacket`'s own buffer may be shared, because that
  is the one place libavformat's zeroed-padding contract applies.
  Trailing *capacity* is not that contract — a decoded plane has more
  pixels behind it and a resampled one has more samples, and a bitstream
  reader running past the payload eats them as bitstream. Provenance is
  recorded at capture, where it is known, and narrowing a payload drops
  it. **Extent**: the view must still leave
  `AV_INPUT_BUFFER_PADDING_SIZE` between its end and the buffer's.
  Where either fails, the packet is copied.

  The empty carrier is handled before any of it: it is backed by no
  `AVBufferRef` at all, so a side-data-only packet — an ordinary thing —
  reaches the send road with nothing to dereference, and gets a
  payload-less `AVPacket` its side data attaches to.

  Worth recording: the census found 0.8 **did not have this**. Its
  reverse builders called the same copy the owned lane uses, so the
  zero-copy chain 0.8 shipped ran demux-to-consumer and stopped. This is
  new, not restored.

- **A conversion that borrows its source copies; one that shares
  consumes.** `ffmpeg_next`'s `Packet`, `frame::Video` and `frame::Audio`
  all lend `&mut [u8]` from safe code and share their buffers by
  refcount with no copy-on-write. A conversion taking `&Packet` and
  returning a view therefore left the caller holding a mutable alias of
  every byte the carrier read — and both sides are `Send`, so the two
  halves need not even be on one thread. No lifetime on the carrier
  would have helped; the carrier is `'static` by design.

  So the roads split by ownership, and the compiler enforces it:

  | road | source | lane |
  |---|---|---|
  | `{kind}_packet_from_ffmpeg_as::<C>` | **by value** | either |
  | `{kind}_packet_from_ffmpeg_in` | **by value** | view |
  | `{kind}_packet_from_ffmpeg` | borrowed | owned |
  | `owned_{kind}_packet_from_ffmpeg_in` (new) | borrowed | owned, with budgets |
  | `convert::{video,audio,image,subtitle}_frame_from` | borrowed | **owned only** |
  | `convert::av_frame_to_*::<C>` (`unsafe`) | raw pointer | either, contract states the obligation |

  The crate's own roads were unaffected in substance: the demuxer owns
  the packet it just read (it now hands it over), the decoders own the
  frames they decode into and reach the conversion through the `unsafe`
  entry, and the resampler owns its output. The zero-copy chain is
  intact end to end. What a *caller* can no longer do is ask for a view
  of something they are still holding — `compile_fail` doctests pin both
  refusals, including the exact aliasing program (`E0382`: the source is
  moved, so `data_mut` cannot be written).

- **Decoded frames carry view planes.** The frame road is lane-generic
  end to end: `VideoFrame`, `AudioFrame`, `SubtitleFrame` and
  `ImageFrame` mean the view lane, `OwnedVideoFrame` and its three
  siblings mean the other, and `empty_video_frame` /
  `empty_owned_video_frame` (with `*_as::<C>()` workers) build the
  destination each one needs. Every plane capture runs through the seam
  **after** the extent proofs that were already there, so the view lane
  inherits the bounds checks rather than restating them.

  Two rules decide what is shared, per medium:

  - a **tight** video or image plane (`linesize == row_bytes`) is a
    window at the decoder's own stride; a **padded** one is copied and
    compacted to `row_bytes` on *both* lanes, because only the first
    `row_bytes` of each row are the decoder's output and the rest is
    allocator scratch nothing wrote. Sharing that span would form a
    slice over uninitialised memory — undefined before a consumer reads
    a byte of it, and the same information leak the owned lane refused
    when it stopped exporting `linesize`;
  - an **audio** plane is a window over exactly
    `nb_samples × bytes_per_sample`, never the alignment padding
    `av_samples_get_buffer_size` allocated past it. `linesize` is used
    for one thing: proving the allocation really is as large as it
    claims.

  A palette plane — a flat `AVPALETTE_SIZE` run inside the frame's own
  buffer — is shared. A subtitle rect is not: `AVSubtitleRect` has no
  `buf[]`, its `data[]` are plain allocations `avsubtitle_free`
  releases, so both lanes copy.

- **The resampler produces view frames too**, through a
  reserve-then-commit pair on the seam. `FfmpegResampler` means the view
  lane, `FfmpegOwnedResampler` the other. The carrier's claim on each
  output plane is taken **before** `swr_convert_frame` runs and settled
  at the produced length after it — which keeps this type's founding
  rule intact on the new lane: `swr` consumes its input as it runs, so a
  failure after it has run would leave a session no caller can retry,
  and there is now no fallible step on that side of the conversion. The
  committed plane is the produced samples, never the capacity that was
  allocated for them.

  The hardware road needed no separate work: `av_hwframe_transfer_data`
  fills a refcounted destination `AVFrame`, which reaches the same
  lane-generic conversion the software road does.

- **The copy roads are fallible on both lanes.** `from_bytes` and
  `from_rows` return `Option`, so an allocation failure is a named
  refusal instead of a silently empty plane — a frame whose header says
  samples and whose planes say nothing is worse than an error. The
  **empty** carrier, by contrast, now allocates nothing on either lane:
  the view lane's is a null-backed zero-length view, which is what lets
  `empty_video_frame` stay infallible rather than reintroducing 0.8's
  four-allocations-that-could-fail placeholder under a new name.

- **What is deliberately not doubled.** The `*Extra` types and
  `SideDataEntry` stay monomorphic. Side data has no `AVBufferRef` to
  share — `AVPacketSideData` and `AVFrameSideData` payloads are plain
  allocations — so **both** lanes copy it, and a second family of extras
  would have been two names for one representation. The carrier
  parameter reaches the payload, not the annotations.

- **The parity lane compares content, not buffers.** Owned compacts a
  padded plane while View carries the tight one at the decoder's stride,
  so the spans are not comparable and asserting on them would have been
  a test of the lanes' plumbing rather than of their agreement.
  `tests/view_carriers.rs` compares row-wise for video and valid-prefix
  for audio, and proves the sharing itself by address: a tight plane's
  pointer lies inside the `AVFrame`'s own buffer, a padded plane's does
  not, an audio plane's lies inside its buffer while its span stops
  short of the padding, and a resampled plane looks into an allocation
  larger than the samples it exposes.


### Changed (BREAKING)

- **`FfmpegBuffer` is deleted, and every byte leaving FFmpeg is copied
  once into an `FfmpegBytes`.** This is the release, and everything
  else below follows from it.

  Through 0.8 a decoded frame's planes and a demuxed packet's payload
  were refcounted *views* into libavcodec's own `AVBufferRef`s, wrapped
  in an `FfmpegBuffer` whose `AsRef<[u8]>` pointed straight at FFmpeg
  memory. It was zero-copy and it was a lie of omission: a consumer
  holding a `VideoFrame` was holding libavcodec's allocation open
  without being told, `FfmpegBuffer` was `Send` and deliberately **not**
  `Sync`, so a frame could be moved to another thread but never read
  from two, and fanning one message out to two consumers was therefore
  impossible without the backend's permission. `mediadecode` 0.9 names
  that rule — the
  [D-seat amputation contract]../mediadecode/CHANGELOG.md — and this
  crate now keeps it.

  The eight exits that changed carrier, all `FfmpegBuffer`  `FfmpegBytes`: the planes of `VideoFrame`, `AudioFrame` and (via its
  payload) `SubtitleFrame`, and the data of `VideoPacket`,
  `AudioPacket`, `SubtitlePacket`, `DataPacket` and `AttachmentPacket`.
  A delivered frame is now owned, `Send + Sync`, `'static`, and clones
  by refcount — see `tests/owned_carriers.rs`, which pins all nine
  forms (the four frame households and the five packet ones) from
  outside the crate.

  **What did not change is the geometry.** A video plane whose
  `linesize` is tight still arrives with the decoder's own stride and
  its full `linesize × height` bytes; a padded one still arrives
  compacted to `row_bytes`. 0.8 already copied the padded case; 0.9
  copies both. Nothing was relaid out, and the existing suites — which
  assert those strides and lengths — are unchanged.

- **`FfmpegBytes` is opaque, and that is deliberate.** It wraps an
  `Arc<[u8]>` today, through a **private** single-arm enum, and exposes
  `AsRef<[u8]>` / `as_slice` / `len` / `is_empty` / `ptr_eq` /
  `copy_from_slice` / `empty` plus `Clone`, `Debug`, `Default`, `Eq`
  and `Hash`. `Send + Sync` fall out of the arm; nothing is
  hand-asserted.

  Why not the bare `Arc<[u8]>` an earlier cut of this release used:
  the storage is going to gain a second strategy. Every exit now
  allocates, copies and frees per frame, and a 4K decode loop is
  asking the global allocator for eight megabytes sixty times a second
  and handing it back; the recorded answer is a plane pool
  ([#35]https://github.com/findit-studio/mediadecode/issues/35),
  whose slabs satisfy the same contract — owned, `Send + Sync`,
  refcount-cloned, no FFmpeg lifetime. With a bare `Arc<[u8]>` in the
  aliases, adding the pool would change the type of every frame and
  every packet in the crate: a breaking release for a change a
  consumer cannot observe. Behind the newtype it is one new arm of a
  private enum. The enum has exactly one arm today and gains the
  second when the pool is built — this crate does not carry members
  nothing can produce.

  It is emphatically **not** 0.8's `FfmpegBuffer` under a new name.
  That type was opaque over *libavcodec's* memory, and the opacity is
  what hid the lifetime. This one is opaque over Rust-owned bytes, and
  the `AsRef<[u8]>` a consumer programs against says everything the
  consumer needs.

- **`SideDataEntry`'s payload is `FfmpegBytes`**, not `Vec<u8>`.
  `new`, `with_data` and `set_data` take one; `data()` still answers
  `&[u8]`, and a new `data_ref()` hands out the carrier for a consumer
  that wants to keep the bytes without copying them again. The reason
  is the contract one tier in: side data rides on packets and frames
  that a graph fans out, and a frame whose *pixels* clone by refcount
  and whose *metadata* deep-copies is the kind of asymmetry nobody
  remembers while profiling. `VideoFrameExtra::smpte_timecode` stays a
  `Vec<u32>` — those are parsed values, a handful of words, not bytes
  crossing a boundary.

- **`try_empty_video_frame`, `try_empty_audio_frame` and
  `try_empty_subtitle_frame` are gone; their `empty_*` siblings are
  infallible and no longer panic.** Those `Option`-returning
  constructors existed because building a placeholder meant four (or
  eight) one-byte `av_buffer_alloc` calls that could fail. The
  amputation removes the allocations — the empty carrier is made once
  for the process and cloned by refcount — and a constructor with no
  failure mode does not get to keep a `Result`-shaped door.

- **`PacketBufferError::Refcount` and `DemuxError::AttachmentAlloc`
  are gone**, with their payload structs. Both named an
  `av_buffer_ref` / `av_buffer_alloc` failure; neither call exists any
  more, so both arms were unreachable. An error variant nothing can
  produce is a claim about failure modes that is no longer true.

  `PacketBufferError::Bounds` stays, and matters **more** than it did:
  0.8 formed a view over a packet's claimed payload range and a
  malformed `size` handed out a slice nobody read; 0.9 reads every byte
  of it. Every side-data cap and every plane-geometry check is
  likewise unchanged and still runs before any allocation.

### Added

- **`FfmpegImageDecoder`**`mediadecode`'s new one-shot
  `ImageDecoder`, backed by `libavcodec`, and with it the cover-art
  road end to end. Opened from an attachment track's codec parameters
  (`FfmpegImageDecoder::open`), fed the track's one
  `AttachmentPacket`, and answering an `ImageFrame`. The
  `AVCodecContext` is reset between pictures rather than reopened, so
  a container with a dozen attachments decodes them through one
  decoder.

  **Censused before it was written: the cover-art reclassification
  never dropped the codec identity.** This crate maps a video-shaped
  stream carrying `AV_DISPOSITION_ATTACHED_PIC` to
  `TrackKind::Attachment`, and that mapping reads
  `AVCodecParameters.codec_id` and puts it on
  `AttachmentTrackParams`, while `TrackExtra` carries a checked deep
  copy of the whole `AVCodecParameters` — width, height, pixel format
  and extradata included. Nothing had to be restored; the road from a
  track row to a decoded picture has no gap in it, and
  `tests/owned_carriers.rs` now pins that.

  Two refusals are named rather than surfacing as a bare FFmpeg
  errno: `ImageDecodeError::EmptyPayload` for an attachment carrying
  no bytes (which the demuxer really does synthesize — a cover-art
  stream that parks no payload, an attachment track with empty
  extradata), and `ImageDecodeError::NoImage` for bytes that were
  accepted and produced no picture.

- **`ImageFrameExtra`** and the `ImageAdapter` impl for `Ffmpeg`. Two
  seats: `orientation` and `side_data`.

- **EXIF orientation takes a typed seat — and the census that put it
  there corrected an earlier reading of libavcodec.** An interim
  version of this entry said orientation arrives in `AVFrame.metadata`
  as a string and therefore could not be given a seat cheaply. It was
  measured instead of reasoned about, and it does not.

  Feeding mjpeg a JPEG whose EXIF IFD carries each orientation in
  turn: for a **recognised** tag (1..=8) the decoder emits an
  `AV_FRAME_DATA_DISPLAYMATRIX` frame side-data entry and puts
  *nothing* in the metadata dictionary. Only an **out-of-range** tag
  (0, 9, …) falls through to the dictionary — as the string
  `"      9"` — and produces no matrix at all, with libavcodec logging
  `unable to attach displaymatrix from EXIF` on the way past. So the
  road was the display matrix all along, this crate was already
  carrying that entry raw, and the seat is a typed *reading* of bytes
  it already had rather than a new FFI mechanism.

  The seat is **`ImageOrientation`**, an eight-valued vocabulary
  (`TopLeft``LeftBottom`, EXIF tags 1–8) plus an `Other([i32; 4])`
  escape carrying the linear part of a display matrix none of the
  eight names — reachable, since the matrix is a general affine
  transform and a MOV `tkhd` can hold an arbitrary one. It offers
  `from_display_matrix`, `to_exif_code` / `from_exif_code`,
  `is_mirrored`, `linear`, and `rotation`, which answers in this
  workspace's existing `mediaframe::frame::Rotation`.

  **Why eight values and not that `Rotation` alone.** `Rotation` names
  four, and four of the eight EXIF orientations are *mirrored*; a
  rotation vocabulary cannot hold a reflection. FFmpeg's own API has
  the same gap: `av_display_rotation_get` answers `-180` for both tag
  2 and tag 3, `-90` for both 5 and 6, and `90` for both 7 and 8, so
  reading the angle alone loses the mirror on half the vocabulary.
  `rotation()` reuses `Rotation` for the half it can carry and
  `is_mirrored()` supplies the half it cannot.

  `orientation()` is `None` when the frame carried no display matrix
  — the ordinary case, and also what an out-of-range tag produces —
  which a consumer can tell apart from "the file said upright". That
  is the difference between leaving a picture alone and having decided
  to.

  This flows on the **image road only**. `VideoFrameExtra` is
  untouched: a video's rotation rides the same display-matrix side
  data and is already carried raw there, and typing it for motion
  video is a separate question with separate consumers.

- **An ICC profile deliberately gets no seat**, though it arrives as
  side data too (`AV_FRAME_DATA_ICC_PROFILE`). It is a payload, not a
  fact — kilobytes of colour transform this crate has no vocabulary
  for and no business parsing — and `side_data()` carries it unparsed
  and whole for a consumer that does. That is the line the image
  household draws: a fact a picture cannot be displayed correctly
  without is typed; a payload only a specialist reads rides raw.

- **`ImageFrame` type alias**, beside the eight that were already
  there, and `convert::image_frame_from` /
  `convert::av_frame_to_image_frame`. The image and video paths share
  one plane-extraction rule (`copy_out_planes`) rather than two that
  could drift.

### Added — resource ceilings

- **Every copy across the boundary is now checked against a finite
  budget, before it allocates.** 0.9 made every exit copy, and a copy
  is a decision to allocate whatever an untrusted container asks for: a
  header claiming 100000×100000 pixels, a packet claiming a gigabyte, a
  Matroska with four hundred attached "fonts". Through 0.8 those claims
  cost a refcount. They now cost memory, so they are judged first. The
  new `limits` module is the one home for them — `DEFAULT_*` consts and
  `Copy` options structs with `new` / getters / `with_*` / `set_*`, the
  shape `VideoDecoder::with_max_probe_pending_bytes` and
  `DEFAULT_MAX_PROBE_PENDING_BYTES` already established.

  Every seat is a **finite default**, never an `Option`. There is no
  "unlimited" spelling on purpose: that is the shape a caller reaches
  for once, in a hurry, and never revisits.

  | seat | default | why that number |
  |---|---|---|
  | `FrameLimits::max_pixels` | 256 Mpx | 8K UHD is 33 Mpx and 16K×16K is 268 Mpx; every real frame passes and the forged header does not. FFmpeg's own default for this is `INT_MAX`. |
  | `FrameLimits::max_frame_bytes` | 512 MiB | 8K 4:4:4 16-bit with alpha is ~253 MiB, 8K P010 ~96 MiB. Pixels alone do not bound the copy — depth, plane count and stride padding multiply it. |
  | `PacketLimits::max_packet_bytes` | 1 GiB | Deliberately ceiling-class. `AVPacket.size` is a `c_int`, so 2 GiB is the structural maximum and this halves it; an 8K ProRes 4444 XQ frame is ~10 MB. The job is to refuse a forged `size`, not to second-guess a codec. |
  | `DemuxLimits::max_attachment_bytes` | 64 MiB | An attachment is a whole file. A generous cover is a 4000×4000 PNG at ~20 MB; the largest CJK fonts are ~30 MB. Two orders under the packet ceiling, which is right — attachments are captured *eagerly*, at open. |
  | `DemuxLimits::max_total_attachment_bytes` | 256 MiB | Nothing in the per-attachment ceiling bounds a container that attaches four hundred modest ones. A full ASS font set is ~100 MB at the high end. |

- **Two layers, one number.** `FrameLimits::max_pixels` is also written
  into `AVCodecContext.max_pixels` for every decoder this crate opens —
  HW probe candidates, the SW fallback, the subtitle and image
  decoders. That is the layer that matters most: libavcodec refuses an
  oversized picture *before allocating it*, where checking only on this
  side would mean FFmpeg had already paid for the frame by the time we
  declined to copy it.

- **New typed refusals**, all naming both the ask and the line, all
  raised before the copy: `ConvertError::TooManyPixels`,
  `ConvertError::FrameTooLarge`, `PacketBufferError::PacketTooLarge`,
  `DemuxError::AttachmentTooLarge` and
  `DemuxError::AttachmentBudgetExhausted`. The two attachment arms are
  separate because they are separate attacks — every payload can be
  individually fine and the file can still be an attachment table — and
  the aggregate arm names the track that crossed the line, not a track
  that was individually at fault, because there need not be one.

- **Where the seats sit.** `FrameLimits` is taken by
  `FfmpegVideoStreamDecoder::open`, `FfmpegAudioStreamDecoder::open`,
  `FfmpegSubtitleStreamDecoder::open` and `FfmpegImageDecoder::open`;
  `DemuxLimits` by the new `FfmpegDemuxer::open_with` /
  `open_reader_with` (the existing `open` / `open_reader` delegate with
  defaults). Taken *at open*, not through a `with_*` builder on the
  session, and for a reason in each case: half of `FrameLimits` is
  written into an `AVCodecContext` whose ceiling cannot move after
  `avcodec_open2`, and the attachment half of `DemuxLimits` is *spent*
  during the open — every attachment payload is captured before the
  call returns, which is what makes the demux tier's "exactly one
  packet, before any timed packet" contract true by construction. A
  budget set afterwards would arrive after the spending.

  A file whose attachments exceed either budget **fails to open**.

- **A cover-art bomb is the same attack as a video bomb**, so the image
  road carries the identical ceilings — and it is the more exposed of
  the two, being what a thumbnailer reaches for without ever asking for
  video.

### Fixed — a payload that carries pointers is refused, not copied

- **`AV_PKT_FLAG_TRUSTED` was a use-after-free reachable through safe
  API.** The copy-out leg duplicated a packet's bytes without retaining
  its `AVBufferRef`, and the boundary faithfully preserved every flag
  including `TRUSTED` — which FFmpeg's wrapped-`AVFrame` producers set
  on packets whose body is not media at all but a **structure of
  pointers to other live objects**. Copying that body produced a carrier
  that satisfied every property the amputation contract states — owned,
  `Send + Sync + 'static`, clone-is-a-refcount-bump — and dangled the
  moment the source pipeline dropped. Handing it back through a reverse
  builder, flag restored, gave a wrapped-frame decoder `av_frame_ref` on
  freed memory.

  The copy moved the pointer; it could not move what the pointer named.
  There is no depth of copying that fixes this, because there is no
  bound on what such a payload's pointers reach — so the contract gains
  a corollary, written into the adapter docs: **a payload that carries
  addresses instead of bytes is uncarriable**, and must be refused by
  name on *both* legs. It is refused on all five copy-out conversions
  and all four rebuild roads, as `TrustedPayload`.

  The lane that asserted `TRUSTED` survived a round trip was itself the
  bug, and now asserts the refusal.

### Fixed — the byte ceiling was only enforced after the allocation

- **A compressible frame could make libavcodec allocate ~800 MB under a
  512 MiB ceiling.** `max_pixels` was pushed into the decoder but
  `max_frame_bytes` was not, and a pixel is not a fixed price:
  10000x10000 is 100 Mpx — comfortably under the 256 Mpx default — and
  800 MB in `rgba64`. Such a frame is a few KB on disk, so nothing
  upstream sees it coming, and the byte ceiling only spoke after
  libavcodec had already paid for the frame this crate then declined to
  copy.

  The byte ceiling is now converted into the pixel ceiling that enforces
  it, for the stream's own pixel format, and the tighter of the two is
  what the decoder is opened with. `av_image_check_size2` — reached from
  `ff_set_dimensions`, which every decoder calls when it learns its
  dimensions and *before* it allocates — is the pre-allocation judge, so
  this bites on the declared shape and on a container that lies about it
  alike. The per-pixel cost is measured through this crate's own plane
  geometry on a 256x256 probe rather than from a table; a stream that
  declares no usable format is charged the widest CPU layout FFmpeg has,
  which over-refuses only above roughly 67 Mpx at the default ceilings.

  **On `get_buffer2`:** the standard custom-allocator hook was censused
  and rejected for this release. It is the more precise instrument — it
  sees the real strides — but it needs per-context side state, and
  `AVCodecContext.opaque`, the only user pointer FFmpeg offers, is
  already owned by the hardware path's callback state. Sharing it means
  one state type threaded through the shared context builder and both
  decoder families, with an FFI callback whose lifetime bugs are
  use-after-free rather than test failures. The pixel-ceiling road
  reaches the same allocation, one layer earlier, with no callback and
  no state. Recorded here so the choice is a decision rather than an
  omission.

  The post-conversion `FrameTooLarge` stays as the backstop for frames
  that arrive without a decoder to have pushed anything down.

### Fixed — the callback reads the frame's layout, not the context's

- FFmpeg's `get_buffer2` contract says the callback uses the values on
  the **frame**, and `avcodec_default_get_buffer2` sizes from them. This
  crate priced audio from the *context's* channel layout, which is
  whatever was last negotiated and can differ outright: a context
  claiming mono against a frame carrying 255 `dblp` channels at 130,000
  samples priced about a megabyte and allocated about 265 MB. The count
  is read raw and signed now, and a non-positive one is refused rather
  than floored.

  **The lesson, recorded at the lane.** R15's exhaustive footprint
  matrix could not have caught this and never could: it feeds one
  channel count into both the estimate and the measurement, so a seam
  that reads one side where the allocator reads the other is invisible
  to it. **A matrix proves a formula; only a lane that lets the two
  sides disagree proves the seam that joins them.** The new lane drives
  `judge_buffer` with the context and the frame described
  independently.

### Fixed — the byte ceiling is recovered per medium, and a zero budget refuses

- The callback derived one byte ceiling for both media from
  `max_pixels`, and that was wrong at both edges. A caller setting
  `max_pixels = 1` gave every **audio** frame a 16-byte budget — a pixel
  ceiling has no business bounding audio, and ordinary sound was
  refused. And a `max_frame_bytes` below one worst-case pixel floors the
  pushed-down `max_pixels` to zero, where the guard was written as
  `max_pixels > 0`: the tightest ceiling a caller can ask for **turned
  the judge off**, which is the one direction a ceiling must never fail
  in.

  Audio now recovers from `max_samples`, which `build_codec_context`
  sets from `max_frame_bytes` alone — no `min` with anything else, so it
  is the caller's own number back. Video keeps the `max_pixels`
  recovery, where the conflation is provably harmless in the direction
  that matters: when the byte road was tighter it is exact, and when the
  pixel road was tighter every frame passing the pixel gate has
  `aligned_pixels <= max_pixels` and so costs at most `max_pixels *
  worst` — it can under-state the caller's byte ceiling, never refuse a
  frame the caller allowed. A zero budget refuses every nonempty
  software frame.

### Added — the demux open is bounded before libavformat builds anything

- **The one seat that reaches behind libavformat.** Every other budget
  in this crate measures a copy it makes itself, and on the demux road
  that is always after `avformat_open_input` and
  `avformat_find_stream_info` have already built the attached picture,
  the extradata and the coded side data out of the file. The attachment
  and parameter seats were measuring this crate's copies of allocations
  that had already happened.

  A parser cannot allocate from bytes it was never handed, so the input
  is bounded. `DemuxLimits` gains `max_probe_bytes` (5 MiB, FFmpeg's own
  `probesize` default) and `max_streams` (1000, FFmpeg's own), and they
  reach libavformat two ways:

  | instrument | reaches | bounds |
  |---|---|---|
  | `probesize` / `formatprobesize` | both entrypoints | what the probe and analysis may consume |
  | `max_streams` | both entrypoints | the `AVStream` array a header can conjure |
  | the `AVIOContext` byte meter | the **reader** entrypoint | total bytes handed over, hard — past the budget the reader stops answering |

  Over-budget is `DemuxError::ProbeBudgetExhausted`, and the meter is
  consulted **before** the errno: libavformat folds a reader's I/O error
  into whatever it was doing at the time — usually "invalid data" —
  which would report a refusal this crate made as a malformed file. The
  budget is released once the container is analysed; reading the media
  afterwards is the caller's own business, already bounded by the packet
  seats.

  **The residual, stated rather than implied.** Allocation
  *amplification* inside a parser is not bounded: a container can
  describe, in a handful of bytes, a structure whose in-memory form is
  far larger, and nothing outside libavformat can observe that. Bounding
  the output of that is the substrate's own hardening territory —
  FFmpeg keeps `max_streams`, `max_index_size` and `max_picture_buffer`
  for it, and this crate now sets the first. And the hard meter does not
  reach the **path** entrypoint at all, because it needs an
  `AVIOContext` this crate owns and a path is opened by libavformat's
  own protocol layer; the probe knobs still apply there, and a caller
  who wants the meter on a file opens it as a reader.

### Fixed — a candidate's refusal is read before the candidate dies

- Replay classified the failure through `self.state` — the backend still
  active — while the error being recorded belonged to `candidate_state`,
  whose `get_format` callback is the one that may have declined. The
  right state was then dropped, so the reason was gone: the attempt log
  recorded FFmpeg's `Invalid data found when processing input` for a
  coded surface this crate refused. The candidate's declination is read
  first now, and the ordering is the whole fix.

### Fixed — the allocator callback prices bytes, for pictures and for audio

- **The software video road still compared pixels.** `judge_buffer`
  bounded the aligned *extent* against `max_pixels` and then delegated —
  but an extent costs whatever its format costs. A 65536x1 `gbrap32le`
  frame is 65,536 pixels, inside a 1 MiB ceiling by that ruler, and
  33.5 MB once allocated: thirty-two times over.

- **Audio passed it unpriced entirely.** `max_samples` bounds the sample
  *count*, so one sample across eight packed `f64` channels is 64 valid
  bytes under a 64-byte ceiling and a 2,080-byte allocation — and it was
  delivered, because the copy-out only ever rechecks the valid bytes.

  Both are now priced through `crate::footprint` before the delegation,
  and an unpriceable format fails closed. The pixel check stays as the
  coarse first gate.

  **The seat, without new state.** The callback can see `max_pixels`,
  and `build_codec_context` sets it to
  `min(caller's pixel ceiling, max_frame_bytes / worst-bytes-per-pixel)`
  — so `max_pixels * worst` is never above `max_frame_bytes`: equal when
  the byte road was the tighter of the two, below it otherwise.
  Recovering the byte ceiling that way is conservative in the safe
  direction and threads nothing through a C callback. Hardware frames
  are delegated rather than failed closed: their pool is judged where it
  is declared, in `get_format`.

### Fixed — the packed-audio formula had the allocator's steps in the wrong order

- **It aligned the channel-multiplied payload once; the allocator rounds
  the sample extent and *then* multiplies by channels.** For packed
  `dbl` at eight channels that priced 576 bytes against a real 2,080; at
  255 channels, 2,560 against 65,312. Twenty-five times under, from a
  formula that looked right.

  The arithmetic is no longer restated. `av_samples_get_buffer_size`
  with `align = 0` **is** the ruler `av_frame_get_buffer` measures with,
  so the pricer asks it through a new `c_int` shim. Measured across
  every sample format this build names, the relation is exact:
  `allocated = av_samples_get_buffer_size(..., 0) + 32 * planes`, with
  planes being the channel count for planar layouts and one for packed.
  The per-plane term is charged at 512 rather than the measured 32.

- **The verification law is now a matrix, and that is the real fix.**
  The R14 lanes were a *curated list* — the shapes the findings happened
  to name — and they passed while the formula was twenty-five times
  under: they priced `s16` at eight channels (dominated) and never asked
  about `dbl` at eight channels (not). The bug lived in the order of the
  arithmetic, so it only appeared once the per-sample width grew, and no
  curated row grew it.

  The sweeps are exhaustive now: every sample format the build names
  crossed with channel counts {1, 2, 8, 32, 255} and sample counts
  {1, 1024, 65535}, and every CPU pixel format crossed with degenerate,
  odd and ordinary shapes — over 950 cells, each compared against a real
  `av_frame_get_buffer` allocation. Reinstating the old formula fails
  the sweep immediately, at `u8` with 32 channels: a cell no curated row
  had. **A curated row proves the case it names and nothing else.**

### Fixed — one funnel for every hardware exit

- **The declination had one consumer in production, not four.** R14
  reported an exits map covering the post-commit classifier, the
  `open_as` failure path and the codec-type mismatch alongside
  `advance_probe`. Only `advance_probe` was wired: the other three were
  written and then lost when the surrounding code was restructured, and
  every gate passed — because the tests exercised the *helper*, which
  was correct, rather than the *roads*, which were not.

  Every exit now goes through one `hw_exit` funnel that reads the
  declination before anything wraps or tears down state, and the
  open-time paths call its free-standing half before the guard releases
  the callback state. Five consumers, and each reachable road has a lane
  that drives it end to end.

  This also closes the quietest of the failure modes: the
  explicit-backend road took the EOF exit, which `is_hw_decode_failure`
  deliberately excludes so a genuinely drained stream is not trapped in
  a fallback loop — so a declined format reported as "stream over", with
  nothing said at all. The funnel tells the two apart, and the new lane
  fails if it stops doing so.

### Fixed — judges now price the allocator, not the payload

The rule the whole series arrives at, now written at the head of both
accounting tables: **a judge must dominate the allocator's arithmetic,
not the payload's.** Two ceilings were comparing what the bytes weigh
against what would be spent, and the gap is not small on the shapes that
matter:

| shape | tight payload | `av_frame_get_buffer(0)` |
|---|---|---|
| `nv12` 16x16 | 384 | **1,792** |
| `yuv420p` 1x1 | 3 | **2,304** |
| `gray8` 65536x1 | 65,536 | **2,097,408** |
| `s16p`, 1 sample, 8ch | 16 | **768** |
| `dblp`, 1 sample, 255ch | 2,040 | **73,440** |
| `yuv420p` 1920x1080 | 3,110,400 | 3,133,696 |

That last row is why this survived so long: on an ordinary frame the
allocator's overhead is under one percent, so every under-pricing defect
hid behind a shape big enough for the slack not to show.

- **The hardware transfer judge** priced `av_image_get_buffer_size` at a
  fixed alignment — 768 bytes for a 16x16 NV12 destination that really
  costs 1,792.
- **The resampler's output preflight** multiplied the tight plane length
  by the plane count, so a 16-byte ceiling admitted a 768-byte
  allocation for one sample across eight planar channels.

Both now go through one shared pricer, `crate::footprint`, modelling
`av_frame_get_buffer` for video *and* audio: both dimensions aligned up
(the allocator aligns the linesize *and* the plane heights, which is
what turns a 65536x1 frame into 2 MiB), per-plane slack for the padding
term, and planar audio charged per plane.

**The estimates are verified, not argued.** `libavutil/frame.c` aligns
width in a loop, aligns plane heights and adds a padding term whose
constants differ by build and CPU; transcribing that is a fragile way to
be exactly right. So the pricer is deliberately a conservative upper
bound and the tests price every shape in the table above against the
real summed `AVBufferRef.size`, asserting the estimate dominates each
one. A build whose allocator grows hungrier fails those tests instead of
quietly outgrowing a ceiling.

### Fixed — the hardware pool is asked, not calculated

- **The coded-surface judge used the codec's alignment; a hardware pool
  aligns by its own API's rules.** `avcodec_align_dimensions2` is what
  the *codec* wants, and D3D11 HEVC and AV1 round both dimensions to
  128 — a 129x129 stream priced 144x160 by codec arithmetic can be
  allocated as 256x256.

  The judge now calls `avcodec_get_hw_frames_parameters` to obtain the
  `AVHWFramesContext` the decoder is about to initialise — populated and
  not yet allocated — and reads the pool's own declared extent, unrefing
  it before returning. Measured on the VideoToolbox road: the pool
  declares 1920x1088 for the cropped fixture, where codec arithmetic
  said 1920x1090. The lane asserts the pool's figure *and* that it is
  not the codec's, so a lost ask is caught rather than approximated.

  One trap found by measuring: the query must be asked about the
  **format being negotiated**, not `ctx->pix_fmt`, which at
  `get_format` time still holds the software format and answers
  `ENOENT` — silently falling back to the arithmetic the call exists to
  replace. The codec-alignment path remains as the recorded fallback for
  accelerators that will not declare.

### Fixed — the coded-surface reason survived only the probe road

- `take_ceiling_declination` ran in `advance_probe` and nowhere else, so
  a decoder opened for one explicit backend — which has no probe —
  classified the refusal as a post-commit hardware failure carrying
  FFmpeg's `Invalid data found when processing input`: the callback's
  own declination reported as though the file were corrupt. `open_as`
  also frees the callback state on its way out, so the open-time path
  lost the reason entirely.

  The reader is free-standing now and consulted at every hardware
  classification exit — the `open_as` failure mapping, the codec-type
  mismatch, and the post-commit classifier — reading the reason before
  the state is released. It clears on read, so a refusal cannot be
  reported a second time against a backend that never declined
  anything.

### Fixed — send-side side data had no ceiling at all

- **The read side has capped side data at 64 entries and 256 KiB since
  it was written; the send side capped nothing.** The preflight looked
  at `body.len()` and then `attach_side_data` allocated every entry the
  caller handed over, one `av_packet_new_side_data` at a time, with
  nothing counting or weighing them — so annotations the demux boundary
  would have refused coming *out* of a container went straight into
  libavcodec going *in*. The same asymmetry `PacketLimits` was
  introduced to close for the packet body, one field along.

  The whole list is now judged before anything is allocated — including
  the body, because a list that cannot be carried should not cost a
  packet first — against the read side's own caps, one seat in both
  directions. Over-count and over-bytes are distinguished by name
  (`SendSideDataTooLarge`), so a caller can tell "too many annotations"
  from "too much annotation".

### Fixed — two dimension vocabularies, and judges reading the wrong one

- **`max_pixels` bounds the display extent; the allocation is the coded
  one.** `ff_set_dimensions` applies `av_image_check_size2` to the dims
  the decoder passes it, and for a cropped stream those are the
  *display* dims. Minted and measured: an h264 clip with SPS cropping of
  32x32 out of a 1920x1088 macroblock grid — a 2040x divergence —
  is admitted by `max_pixels = 5000`, because 32x32 is 1024 pixels.

  On the software road that gap was already closed, and this was
  measured rather than assumed: `get_buffer2` receives the frame at
  **coded** dims (1920x1088, aligned to 1920x1090, a 2,092,831-byte
  allocation), so `judge_buffer` bounds the real extent. The hardware
  road never reaches `get_buffer2` at all, which left the coded surface
  unjudged there.

  The `get_format` callback this crate already owns is the seat: it is
  the last moment before the hardware pool is built and the first at
  which the coded extent is known. It now applies the same `max_pixels`
  the context carries to the aligned coded dims.

  **The refusal mode, censused rather than chosen.** A `get_format`
  callback cannot return a reason — declining means `AV_PIX_FMT_NONE`,
  which libavcodec reports as `Invalid data found when processing
  input`: true about what it saw, false about what happened, since the
  data was fine and this crate declined it. So the reason is left in the
  callback's own state (already owned, already in `opaque`) and the
  probe funnel turns it back into `Error::HwSurfaceTooLarge` before
  recording the attempt. The caller gets the aligned coded pixel count
  and the ceiling, not FFmpeg's misnomer.

- **`judge_hw_transfer` priced the display extent too.** What
  `av_hwframe_transfer_data` allocates is sized from the frames context,
  not from `AVFrame.width`. This crate already had
  `hw_frames_ctx_dimensions`, whose doc comment names this exact trap
  and whose own test uses an 8192-pool-over-100-display fixture — and
  the first cut of this judge reached past it for the display dims
  anyway. It now prices every transfer candidate at the pool dims and
  takes the worst.

  It also **fails closed**: a hardware frame whose pool extent cannot be
  read is charged as unbounded, because an unprovable extent is not a
  small one. A frame that is not a hardware frame at all is passed
  through instead — `av_hwframe_transfer_data` refuses such a source
  and allocates nothing, so answering "too large" there would put a
  ceiling's name on a different fault.

- **The sweep.** Every site reading a dimension is now tabulated in the
  `convert` module docs with the vocabulary it needs. Two more reads in
  `drain_into_pending` turned out to be log fields, sizing nothing;
  `copy_out_planes` and the public `width`/`height` accessors are
  correct as they stand. The rule the table states: *the extent to judge
  is the one the allocator will use, and it is read from whatever
  structure the allocation is sized from, never assumed.*

### Fixed — a side-data allocation failure no longer publishes a partial frame

- **OOM mid-copy was absorbed.** `av_frame_new_side_data` returning null
  logged a warning, stopped copying, and returned `Ok(())` — publishing
  a frame carrying whatever side data happened to fit before the
  allocator gave out. What sits in that list is the HDR mastering
  metadata, the ICC profile and the display matrix: a picture that comes
  back with its colours or its orientation quietly missing is worse than
  one that does not come back, because nothing downstream can tell.

  The failure is returned now. The caller already knew what to do with
  it — it unrefs the partial destination and either advances to the next
  backend or surfaces the failure for a software retry — so this is a
  `break` becoming a `return`, against machinery that was already in
  place. Covered by fault injection through the crate's existing
  `av_max_alloc` subprocess harness, which forces the null for real
  rather than testing the propagation shape by hand.

### Fixed — the hardware road gets the seat the allocator hook cannot reach

- **`judge_buffer` is not a universal choke point.** `ff_get_buffer`
  calls `hwaccel->alloc_frame` directly and never reaches `get_buffer2`
  at all, and `av_hwframe_transfer_data` allocates its CPU destination
  outside both hooks. Censused on this machine: VideoToolbox is the only
  hardware config h264, hevc and vp9 advertise, the device opens, and a
  VideoToolbox h264 decode of a 160x120 clip records **zero**
  `get_buffer2` calls while producing a hardware frame.

  **What the census settled about the surface itself:** `max_pixels`
  *does* bite before `alloc_frame`, measured rather than assumed. With
  `max_pixels = 100` the same decode fails at `avcodec_open2` with
  `Picture size 160x120 exceeds specified max pixel count 100` from
  `av_image_check_size2` — zero `get_buffer2` calls, no frame. The check
  is in `ff_set_dimensions`, which every decoder runs when it learns its
  dimensions and before any surface pool exists, so the seat
  `max_pixels` already occupies covers the hardware surface too. The
  residual there is the aligned-dimensions gap, and it applies to
  **driver-owned GPU memory** rather than to anything this crate
  carries.

  **What this crate does carry off that road is the CPU frame**, and it
  now has an exact judge: `judge_hw_transfer` runs before *every*
  `av_hwframe_transfer_data` call. The destination format is not this
  crate's to choose — `dst.format` is `AV_PIX_FMT_NONE` on entry and
  FFmpeg picks from `av_hwframe_transfer_get_formats` — so the whole
  candidate list is priced at the transfer's own alignment and the worst
  taken, walked as `*const c_int` through a new shim because a driver
  may offer a format this build does not name. Over-ceiling is
  `Error::HwTransferTooLarge`.

  It is judged at the call site rather than inside `transfer_hw_frame`
  on purpose: errors from that function are FFmpeg's, and the arms
  around it reclassify them into "the hardware failed, fall back to
  software". A byte ceiling is not a hardware failure — software would
  decode the same oversized frame and be refused again — so the named
  refusal returns straight to the caller instead. On the probe-replay
  road the same judge reports through that path's existing error
  channel, where every failure is collapsed into "try the next
  backend"; the reason is logged rather than named, because a name
  there has no consumer.

  This is also what closes the degenerate-shape residual on the
  hardware road. The pixel ruler bounds `w * h * 16`, but a transfer
  pads every row: a one-pixel-wide frame of N rows costs 64N bytes for
  N pixels in NV12 at 32-byte alignment, which is four times the ceiling
  the pixel ruler would have allowed. The shape is not producible with
  any encoder available here — h264 and hevc will not encode a
  one-pixel-wide stream — so the guard is stated and reasoned rather
  than driven by a lane. What *is* pinned is its accepting path: a real
  decode through `VideoDecoder`'s auto-probe still delivers frames.

### Fixed — the sample ceiling counted channels twice

- **`max_samples` was over-divided, and it refused valid media.**
  FFmpeg compares `nb_samples * ch_layout.nb_channels` — the
  channel-sample count — against the field, which is already the
  per-channel accounting the ruler wanted. Dividing by the channel count
  as well charged it twice: at the 512 MiB default the ceiling was
  263,172 channel-samples instead of 67,108,864, and a **valid**
  6-channel FLAC block of 65,535 samples (393,210 channel-samples, about
  1.5 MiB of `s32`) was refused as a bomb.

  What made this easy to get wrong is that FFmpeg's own log message
  prints only `nb_samples` — "samples per frame 4096, exceeds
  max_samples 10000" — while having compared 4096 x 6 = 24576 against
  10000. Measured on this build to the unit: a 6-channel 4096-sample
  frame is accepted at exactly 24576 and refused at 24575.

  The ruler is now `max_frame_bytes / worst_bytes_per_sample`, and
  FFmpeg supplies the channel count itself.

  **The lesson, recorded in the test rather than only in prose.** The
  pin that was supposed to catch this asserted the same arithmetic the
  production code used, so when that arithmetic was wrong the pin agreed
  with it — a pin that restates the implementation tests nothing. It now
  pins the *consumer's* semantics: the exact channel-sample boundary,
  driven through a real 6-channel decode at 24576 and at 24575, plus the
  6-channel FLAC block that the old ruler refused.

### Fixed — the enum discipline, re-applied to the code that enforces it

- **The pixel-format census was undefined behaviour on its own reason
  for existing.** It walks this build's descriptor table to price the
  most expensive format libavcodec can emit — the point being to be
  correct about formats the bindings may not name — and the generated
  `av_pix_fmt_desc_get_id` hands those ids back as a closed
  `AVPixelFormat`. Every future format would have become an invalid Rust
  enum value on the way *into* the pricing meant to handle it.

  Local ABI-compatible shims now declare `av_pix_fmt_desc_get_id`,
  `av_image_get_buffer_size` and `av_get_bytes_per_sample` with `c_int`,
  joining `avcodec_find_decoder`. Both Rust declarations resolve to the
  same C symbol; an unknown id never becomes an enum on the Rust side.

  The sweep table in the `convert` module docs gains these rows, marked
  as what they are: the open-C-enum class **inside this crate's own new
  code**. Writing the discipline down was not enough — it had to be
  re-applied to the code that enforces it.

### Fixed — the ceiling now follows the allocator, not the header

- **Dimension alignment walked straight past the pixel ceiling.**
  `max_pixels` is checked against the frame's *raw* `width * height`;
  what libavcodec then allocates is the shape
  `avcodec_align_dimensions2` rounds that up to. For degenerate aspect
  ratios those are not the same number. Measured on this build:

  | shape | raw | aligned | inflation |
  |---|---|---|---|
  | `gray8` 65536x1 | 65,536 px / 64 KiB | 65536x32 = 2,097,152 px / 2 MiB | **32x** |
  | `gray8` 1x65536 | 65,536 px / 64 KiB | 16x65536 = 1,048,576 px / 2 MiB | 16x |
  | `yuv420p` 7680x4320 | 33,177,600 px | unchanged | 1.00x |
  | `gray8` 1024x1024 | 1,048,576 px | unchanged | 1.00x |

  So a one-pixel-tall frame slipped 32 times its declared cost past a
  1 MiB ceiling — and no value of that scalar could have fixed it,
  because bounding a product cannot bound a product whose factors are
  then rounded up independently. Real pictures inflate by nothing at
  all, which is exactly why the ceiling looked sound.

  **The road, chosen by measurement.** `get_format` was censused first,
  being the smaller instrument: it receives the context, needs no
  allocation decision, and could apply the same scalar to aligned
  dimensions. It **does not fire on every road** — on this build a
  one-shot `mjpeg` decode calls it once and a `png` decode calls it
  *zero* times, and cover art is overwhelmingly one or the other, so
  half the guarded road would have been unguarded. `get_buffer2` fired
  on both, sees the frame's real format rather than a negotiated
  candidate, and is the allocator itself.

  **The `opaque` conflict was dissolved rather than solved.** No state
  is needed: the callback reads the ceiling from
  `AVCodecContext.max_pixels`, the field this crate set itself, and
  applies it to the aligned dimensions. Same scalar, same meaning,
  applied where alignment is knowable. `opaque` is untouched, so the
  hardware path keeps it, and there is no allocation whose lifetime has
  to outlive a C callback. Panic discipline is structural rather than
  asserted: the body allocates nothing, indexes nothing, unwraps
  nothing, and calls three FFmpeg functions — and an `extern "C"` fn
  aborts rather than unwinding into C regardless.

- **Audio had no pre-allocation guard at all.**
  `AVCodecContext.max_samples` — FFmpeg's audio `max_pixels`, compared
  against a frame's `nb_samples` in `ff_get_buffer` before the planes
  are allocated — was left at `INT64_MAX`, so the audio family leaned
  entirely on this crate's post-decode byte check: refusing after
  libavcodec had already paid.

  The ruler is `max_frame_bytes` divided by the worst cost of one
  sample: the widest sample format this build can emit (`s64`/`dbl` and
  their planar twins at 8 bytes — censused through the new shim, not
  assumed) times the most channels this crate will carry (255, where
  the conversion refuses by name). At the 512 MiB default that is
  **263,172 samples per frame**, which clears everything real by a wide
  margin — FLAC's largest block is 65,535, Opus tops out at 5,760, AAC
  at 2,048, and a full second of 192 kHz audio would be 192,000.

  Planar alignment padding sits outside the ruler (at most
  `align x channels`, about 8 KiB at 255 channels) and is left to the
  post-decode check, which stays as the backstop.

  Both rulers are pinned by a unit test, because both are censuses of
  the build rather than constants: a ceiling whose documented
  derivation no longer matches its arithmetic is worse than one with no
  documentation at all.

### Fixed — the pre-allocation ceiling stops negotiating with the file

- **The byte-derived pixel ceiling was priced from the container's
  declared format, and a declaration is not an upper bound.** It may be
  unset, it may be wrong, and it may be narrower than what the decoder
  actually emits — a stream declaring `yuv420p` at 1.5 bytes per pixel
  whose decoder outputs `rgbaf32` at 16 got a ceiling more than ten
  times too generous. The undeclared-format fallback charged 8 bytes per
  pixel while this crate delivers 16-byte layouts, so 512 MiB admitted
  ~67 Mpx, which at 16 bytes is ~1 GiB inside libavcodec before the
  post-decode backstop said anything. The hole was one layer below the
  one the ceiling had just been added to close.

  The rate is no longer negotiated with the file at all: **every stream
  is charged the worst per-pixel cost any format this build can emit**,
  and that worst case is measured rather than tabulated. The descriptor
  table is walked once per process and each format priced through
  `av_image_get_buffer_size` — the same function
  `avcodec_default_get_buffer2` sizes from — so a future FFmpeg that
  adds a wider format is priced correctly without this crate learning
  its name.

  **The census:** 267 descriptors, 251 of them CPU formats that price
  (the rest are hardware surfaces carrying no CPU bytes). The maximum is
  **16.000 bytes per pixel**, reached by eight formats — `gbrapf32be/le`,
  `rgbaf32be/le`, `rgba128be/le`, `gbrap32be/le` — with the 12-byte
  `gbrpf32`/`rgbf32` family next below. Measured on a 256x256 probe,
  which divides every chroma subsampling and every alignment libavcodec
  uses, so the figure is the rate and not a rounding artefact; the same
  census at 257 reads 16.934, and that 5.8% is per-*row* padding, a term
  linear in height rather than in pixels.

  **The trade, stated:** over-refusal for cheap formats. At the 512 MiB
  default the effective ceiling is ~33.55 Mpx, so 8K (33.18 Mpx) still
  decodes in *any* format — including the 16-byte ones, where it really
  does cost 506 MiB — but a 16K `yuv420p` frame that would only have
  cost 199 MB is refused with it. That is the honest shape of a bound
  that has to hold before the format is known, and the deployment answer
  is `max_frame_bytes`, which is exactly the knob for how much memory
  one frame may cost. A compile-time assertion now pins 8K-at-16-bytes
  under the default, so lowering the default or meeting a wider format
  fails the build rather than quietly refusing 8K at run time.

  **`get_buffer2` was reconsidered and again not taken.** The blocking
  problem is unchanged and was not solved: it needs per-context side
  state, `AVCodecContext.opaque` is already owned by the hardware path's
  callback state, and composing the two means one state struct threaded
  through the shared context builder and both decoder families behind an
  FFI callback whose lifetime and panic discipline would have to be
  proved rather than asserted. The measured worst-case rate reaches the
  same allocation, one layer earlier, with no callback and no state.

### Fixed — a still bought its planes before judging its annotations

- **The side-data budget ran after `copy_out_planes`.** The refusal was
  correct and it arrived after the expensive half of the work: an
  over-budget still had already paid for up to `max_frame_bytes` of
  plane copies before its annotations were so much as totalled.

  The measuring pass — which reads only the entry count and each entry's
  declared `size`, dereferences no payload and allocates nothing — is
  hoisted to run with the other free judgements, before a single plane
  is bought. The copying pass re-runs it: repeating two comparisons that
  guard an allocation is defence in depth, and it keeps that function
  correct on its own terms rather than only in the order it happens to
  be called in.

  The rule this settles, now in the `convert` module docs: **everything
  a conversion can refuse is refused before anything it can allocate is
  allocated.**

### Fixed — the open-C-enum discipline extends to the dependency's API

- **The image decoder opened through `Decoder::video()`.** That resolves
  the codec by reading `AVCodecParameters.codec_id` as the bindgen
  `AVCodecID` and then reads `AVCodecContext.codec_type` as
  `AVMediaType`. Both are open C enums FFmpeg extends in
  ABI-compatible releases, and forming a Rust enum from a value outside
  this build's discriminant set is undefined behaviour before any
  comparison can run. The hardware path has bypassed that API since it
  was written — the image road, which is the one a *file* chooses the
  codec id on, was still going through it.

  The sweep that followed found the same call on three more roads (the
  software video, audio and subtitle decoders) and
  `Parameters::medium()` on four (track building, attachment
  classification, the resampler's spec check, and a `Debug` impl). All
  are closed: the codec is resolved off a raw `u32`, the medium is
  proved off a raw `i32` through a shared `ensure_codec_type`, and
  `medium()` is replaced by a total fold into a `MediaKind` this crate
  owns.

  The `medium()` calls had a recorded defence — `AVMediaType`'s set is
  tiny and stable — and it was true enough to have never bitten. That
  is a reason it was unlikely, not a reason it was sound, so the
  exception is gone rather than re-argued. No attacker-reachable path in
  this crate now forms a bindgen enum out of FFmpeg memory.

### Fixed — three numbers judged after they were spent

- **The corruption gate read the loud field and not the quiet one.**
  `AV_FRAME_FLAG_CORRUPT` is set rarely; `AVFrame.decode_error_flags` is
  what h264 actually writes for a frame it concealed its way through,
  with the frame flag left clear. The gate now treats any nonzero value
  as corruption — not an enumeration of the members this build names,
  which would re-open the door on the next release.

- **Crop arithmetic was unchecked and accepted a zero-extent rect.**
  The crops are `size_t`, so `left + right` is a real overflow: a panic
  in debug, a wrap in release, and a wrapped sum passes the extent test
  and narrows into a rect pointing outside the picture. Each pair is
  `checked_add`ed now, sums are refused at `>=` the extent as FFmpeg's
  own `av_frame_apply_cropping` does, and the narrowing happens only
  after both proofs.

- **An undersized stride was consumed as if it were a padded one.** A
  `linesize` *below* the format's row width fell into the branch for
  strides that are larger, so the ceiling was charged for bytes the
  plane did not have and the real refusal was left to the copy loop —
  by which time every earlier plane had been allocated and copied and
  thrown away, and the error reported was whichever ceiling the inflated
  total tripped rather than the layout fault underneath it.

  The pass structure changed with the fix: the byte ceiling is judged
  from the **geometry alone**, which no number the frame chose can
  influence, so it runs first and reads nothing; then every stride is
  judged in its own pass; only then does the copy loop allocate. The
  copy loop keeps its own form of the check, one comparison guarding a
  `from_raw_parts`.

### Fixed — a still's ICC profile no longer takes its orientation with it

- **The shared 256 KiB side-data cap silently swallowed legitimate
  profiles.** On a video stream that cap is defensible: side data there
  is small, repeated and per-frame. On a decoded still it was wrong
  twice. A still *is* its annotations — the ICC profile that decides its
  colours and the display matrix that decides its orientation both live
  there, both carried by exactly one frame — and the parameter road next
  door already admits ICC profiles up to 16 MiB, so the same profile was
  accepted as a track parameter and dropped as a frame annotation. The
  drop was also positional: entries past the cap were skipped, so a
  large profile pushed the display matrix off the end and the picture
  came back silently rotated wrong.

  The still road gets its own configurable seat,
  `FrameLimits::max_image_side_data_bytes`, defaulting to the same 16
  MiB as `DEFAULT_MAX_CODEC_PARAMETER_BYTES` — with a compile-time
  assertion that the two stay aligned, because a ceiling that admits a
  profile on one road and drops it on the other is not a policy but an
  accident of which road the file took. Over-budget is a named refusal
  (`ImageSideDataTooLarge`, `ImageSideDataEntries`), judged whole before
  anything is copied, never a truncated list. The shared stream road
  keeps its caps and its behaviour.

### Fixed — a validator that runs after its own field's first consumer

- **The channel ceiling was reading a laundered number.** The refusal
  added for 256-channel packed audio took its count off the materialised
  `ChannelLayoutDescription` — which stores `nb_channels.max(0)`. So the
  guard against a bad count was reading a value the layout helper had
  already floored: a declared `-1` arrived as a legitimate-looking `0`
  and, on a zero-sample frame, produced a frame instead of a refusal.

  Worse in the other direction, materialising *runs first*. For an
  `AV_CHANNEL_ORDER_CUSTOM` layout that means rendering the layout name
  through FFmpeg and walking `nb_channels` map entries into a
  `Vec::with_capacity(nb_channels)` — work proportional to the number
  the ceiling exists to bound, performed before the bound is applied.

  The count is now read straight off `AVChannelLayout.nb_channels` as
  the signed integer it is, every refusal is stated against that raw
  value, and only a count already proved to be in `0..=255` is allowed
  to drive the description. `UnsupportedChannelCount` carries an `i32`
  accordingly.

- **The same inversion on the picture roads, found by auditing for it
  rather than by being told.** `width` and `height` were floored with
  `.max(0)` before anything judged them, so a declared `-1` became `0`
  — and zero pixels is under every ceiling, so a `VideoFrame` (or
  `ImageFrame`) of zero extent was built and returned. Negative
  dimensions are now refused by name as `InvalidDimensions`, on both
  roads. Zero is deliberately not refused: it is what an unset dimension
  reads as, and inventing a refusal for it would be policy this audit
  has no evidence for.

  Two more on the same sweep: the byte-ceiling pass consumed
  `linesize[i]` with a `.max(0)` while the copy loop below did the
  judging, and `crop_*` — a `size_t` — was truncated with `as u32`, so a
  crop of `2^32 + 5` became a perfectly plausible `5` and produced a
  visible rect that was wrong in a way nothing could see. The stride
  refusal is hoisted into the pass that first reads it; the crops are
  kept in `u64` and checked for coherence against the frame's own
  extent, and an incoherent one now withholds the annotation instead of
  fabricating a zero-extent rect.

  The whole census — every raw header field on the audio, video and
  image paths, with its validator and its first consumer — is recorded
  as a table in the `convert` module docs, because the answer to "is
  this ordered correctly" should not have to be re-derived by reading.

### Fixed — the image seam stopped dropping packet flags

- **Every attachment reached libavcodec with a zeroed `flags` field.**
  The one-shot image road rebuilt its `AVPacket` from the payload bytes
  alone, so everything the portable packet said about itself stopped at
  the seam. The three stream families have always rebuilt through
  `write_md_flags`; the image road now does too, rather than growing a
  second copy of it.

  `AV_PKT_FLAG_DISCARD` is the flag this recovers: measured on this
  build, libavcodec genuinely obeys it and produces no frame, so
  forwarding it is both necessary and sufficient. Before this, a packet
  the caller had marked as one to drop was decoded anyway.

- **`CORRUPT` is refused at the seam, and the census is why.** Measured
  with a real cover-art payload: the flag reaches libavcodec and
  libavcodec does nothing with it — mjpeg returns a full picture and
  leaves `AV_FRAME_FLAG_CORRUPT` clear. Forwarding it would therefore
  not preserve the fact, only move where it is dropped, and `ImageFrame`
  has no flag seat to land it in.

  So a payload marked corrupt is refused by name (`ImageDecodeError::Corrupt`)
  rather than decoded into a picture whose only warning has been deleted
  en route. The sibling road is closed with it: a decoder that hands
  back a picture and marks the *frame* corrupt is refused the same way,
  because that fact has exactly as little room to live in an
  `ImageFrame`. A caller who wants the bytes decoded regardless can
  clear the flag it set.

### Fixed — the audio header is judged, never clipped

- **A malformed audio header could arrive as a well-formed frame.** Two
  numbers on the decode path were floored instead of refused, and both
  turned a bad frame into a plausible one — which is worse than an
  error, because nothing downstream has a reason to doubt it.

  `nb_samples` was clamped with `.max(0)`, so a negative count became
  the *empty frame* shape: a refusal delivered as a successful decode of
  nothing. And the sample format was only checked inside the non-zero
  arm, so a zero-sample frame with `AV_SAMPLE_FMT_NONE` — the state a
  codec context is in before its decoder opens — or an unknown format
  came back as an `AudioFrame` advertising a format nothing can
  interpret. A frame carrying no samples still has to say what it is.

  Every header field is now judged before a byte of geometry is
  computed: `InvalidSampleCount` names a negative count,
  `UnsupportedSampleFormat` names a format with no byte width, and the
  zero-sample shortcut is taken only for a count that is *exactly*
  zero. `linesize[0]` is refused when negative at any sample count
  rather than floored.

- **Packed audio above 255 channels was silently truncated.** The
  channel count was clipped to `u8::MAX`, and only *planar* layouts
  refused the excess — via the plane ceiling, which packed audio never
  reaches, because packed declares one plane at any channel count. So a
  256-channel one-sample S16 frame computed its byte product from 255,
  copied 510 of its 512 valid bytes, and handed back a frame advertising
  a channel count the file never declared. Two lies, neither reported.

  `UnsupportedChannelCount` now refuses the count before any plane
  geometry exists, and the packed byte product uses the declared count
  with no substituted `1`. A frame claiming samples across zero channels
  is refused as incoherent rather than treated as mono.

- **The same clip on the resampler, closed at the same choke point.**
  `check_spec` refused planar specs past eight planes but let a packed
  256-channel spec through, and every output frame then took
  `.clamp(0, 255)` on the way into its channel seat. It is refused at
  construction now, for both ends, as `ResampleError::UnsupportedChannelCount`
  — construction being the only place a *target* refusal can be acted
  on, since the later one arrives after `swr` has consumed the input.

  With the ceiling stated once, the three defensive floors it hid behind
  (`channels.max(0)` twice, `channels.max(1)` in the byte product) are
  gone: `plane_bytes` refuses a non-positive count through the `Option`
  it already returns instead of inventing a channel.

  The audio path now carries **no lossy numeric clamp**. The one
  surviving floor, `sample_rate.max(0)`, was censused and kept with the
  reason recorded at the site: it feeds no geometry, no allocation and
  no copy length, and zero is already this crate's "rate unspecified".

### Fixed — a zero-sample audio frame is a frame

- **An empty audio frame was refused as a layout error.** FFmpeg's
  canonical zero-sample frame carries a format, a layout and a rate with
  `data[i] == NULL`, `linesize == 0` and no `AVBufferRef` — nothing is
  allocated because there is nothing to hold. The plane loop still
  demanded every pointer be non-null and buffer-backed, so a header
  frame mid-stream came back as `InvalidPlaneLayout` and interrupted a
  decode that was going fine.

  The loop now does not run when there are no valid bytes. The declared
  layout is still reported — `plane_count` stays packed's 1 or planar's
  channel count — and those slots hold the shared empty carrier at
  stride 0, so a consumer sees the shape it expects carrying no samples,
  which is what the frame says. No allocation: the empty carrier is one
  refcount bump, and six declared planes share it.

  The non-zero path is untouched. The whole change removes one line —
  the loop header — and the body is byte-identical.

### Fixed — valid bytes, the send leg, and two exact caps

- **An audio plane exported its alignment padding.** The copy took
  `linesize[0]`, which is what `av_samples_get_buffer_size` *allocated*
  — rounded up for alignment — rather than
  `nb_samples * bytes_per_sample` (times the channel count when
  packed), which is what the decoder actually wrote. That formed a
  `&[u8]` over bytes nothing initialises, which is undefined behaviour
  before anything reads them, and handed that stale heap to a consumer
  inside a safe `FfmpegBytes`. `linesize` is now used for exactly one
  thing: proving the source allocation is as large as it claims. What
  is copied is the valid product, and the plane's stride agrees with
  it.

  **Class sweep of every exit**, and the distinction that matters:

  | exit | exports | verdict |
  |---|---|---|
  | audio planes | was `linesize`, now the valid sample product | **fixed** |
  | video/image planes, tight stride | `plane_h × linesize` where `linesize == row_bytes` | correct — no padding exists by definition of the branch |
  | video/image planes, padded stride | `row_bytes × plane_h`, copied row-wise | correct — the per-row padding is stepped over, never read |
  | subtitle bitmap | `linesize[0] × h` from the rect | correct — FFmpeg's documented valid extent for `AVSubtitleRect` |
  | palette | a fixed `AVPALETTE_SIZE` | correct — the format's own size |
  | packet payloads | `AVPacket.size` | correct — the declared payload, not the buffer |
  | resampler output | `per_sample × produced` | correct, and always was — the decode path now agrees with it |

  Video's stride-including rows are *initialised decoder output
  geometry*: a consumer reads them through the stride the plane
  carries. Audio has no such geometry — the padding past the samples is
  not part of any layout, which is why the two cases resolve
  differently.

- **The reverse packet builders enforced no budget.**
  `ffmpeg_packet_from_{video,audio,subtitle}_packet` rebuild a packet
  into an `AVPacket` through `try_packet_copy`, which checked only
  `c_int::MAX` — so a configured `max_packet_bytes` was dead on the road
  a caller feeds a decoder directly, and bytes the demux boundary would
  have refused went straight into libavcodec. All three now take
  `PacketLimits` and judge before the allocation, with a new
  `PacketBuildError::SendPayloadTooLarge` named for the direction. This
  is the **into-FFmpeg packet road** of the budget class and it is in
  the accounting table under the same rule.

  The subtitle session **discarded `DecoderLimits` after open**; it
  retains them now, like the other two, and exposes them through
  `limits()`.

- **`try_clone_parameters` hardcoded the crate default.** The active
  `max_codec_parameter_bytes` is threaded through every clone on the
  decoder tier — the initial ownership clone, the probe state's copy,
  every probe advance and the software fallback — so a lowered ceiling
  binds before `build_codec_context` and a raised one is actually
  usable between 16 MiB and the configured value.

- **Synthesized-attachment admission charged padding the clone never
  allocates.** The carrier is an `FfmpegBytes` over the payload alone
  and the clone omits extradata entirely on that road, so the padded
  figure billed 64 bytes nobody spends — rejecting a payload in the
  last 64 bytes below the ceiling that the image road, judging the same
  bytes, accepts. `ParameterFootprint` now reports the raw
  `extradata_payload` alongside the padded clone cost, and the
  admission pass charges carriers the former. Both attachment tiers are
  exact, and the demux and image roads agree at the cap.

### Fixed — the roads into FFmpeg, and two still layouts

- **Every decoder entry point now measures its parameters.**
  `avcodec_parameters_to_context` is a wholesale copy *into* libavcodec
  — it duplicates `extradata`, every `coded_side_data` entry and the
  channel map into the context — and `VideoDecoder::open_with_limits`
  and `FfmpegImageDecoder::open` reached it with whatever the caller
  declared. The outbound clone had been closed; this was the inbound
  one. **Choke point**: `build_codec_context` measures and admits
  before the call, and every decoder in this crate opens through that
  one function (the four session `open`s, the HW probe's `build_state`,
  its per-backend advances, the software fallback), so there is no
  second road to guard.

- **The image decoder's compressed input has a ceiling.** `decode`
  duplicates its payload into an `AVPacket`, capped by nothing but
  `c_int::MAX` on the road that skips the demuxer. New
  `DecoderLimits::max_image_input_bytes`, defaulting to
  `DEFAULT_MAX_ATTACHMENT_BYTES` (64 MiB) — the attachment family,
  because what this seam decodes *is* an attachment, and a packet-family
  default would have made the direct road a gigabyte more permissive
  than the demuxed one for the same bytes. New
  `ImageDecodeError::InputTooLarge`.

  Both seats ride a new `DecoderLimits`, which composes `FrameLimits`
  with the two things a decoder spends before it has produced anything.
  The four session `open`s take it in place of `FrameLimits`; the
  resampler keeps `FrameLimits`, having no parameters to copy.

- **Open C enums are no longer read as closed Rust ones.**
  `measure_parameters` formed `&AVPacketSideData` and loaded
  `AVChannelOrder` directly. Both are *open* enums: an ABI-compatible
  FFmpeg newer than the bindings emits values these do not name, and a
  typed reference asserts every field inhabits its declared type —
  undefined behaviour before a field is read. Descriptor fields now go
  through `addr_of!` and integer reads, side-data type ids are copied
  as **raw bits** (a kind this build cannot name is still a kind the
  file carries), and an unrecognised channel order **fails closed**,
  because its union arm is unknown and both "owns nothing" and "owns
  `nb_channels` of something" would be guesses. Swept the diff for
  siblings: the remaining bindgen-enum reads in this crate already went
  through raw `i32` windows (`from_av_pixel_format`,
  `collect_side_data`, `packet_side_data`, `channel_layout`,
  `build_tracks`' codec id).

- **A synthesized attachment is cloned with `extradata` omitted from
  the outset**, not copied and stripped afterwards. The old shape
  allocated the payload for no reason and — worse — charged it against
  the *parameter* ceiling on the way past, so a font between the two
  ceilings (over the 16 MiB parameter one, under the 64 MiB attachment
  one) passed the admission pass and then failed deterministically
  inside the clone. New `ExtradataPolicy` makes the clone's accounting
  the same accounting the admission did.

- **Indexed and 1-bit stills decode.** Cover art is routinely an
  indexed PNG (`pal8`) and sometimes a 1-bit one (`monob`); both were
  refused outright. **Nothing is converted** — mediadecode delivers
  what FFmpeg decoded, and turning `pal8` into RGB is colconv's job one
  tier along — so a paletted still arrives as its indices plus its
  palette and a 1-bit still as packed rows.

  **The still road was widened, not the shared one.** Measured choice:
  widening the shared road would change what every existing video
  consumer can be handed (`is_supported_cpu_pix_fmt`, the HW-transfer
  validation and the video suites all key off the same deliverability
  answer) to serve formats motion video does not occur in. The still
  road is one `PlaneRoad` enum away. Both layouts are **format**-bounded
  rather than budget-bounded: libavutil already computes a 1-bit row as
  `ceil(width / 8)`, and the palette is a flat `AVPALETTE_SIZE` run —
  256 × `AV_PIX_FMT_RGB32`, always. The palette needed its own copier
  arm because FFmpeg leaves its `linesize` at zero, which the strided
  path reads as an unusable layout.

- **The footprint counts the extradata padding.**
  `AV_INPUT_BUFFER_PADDING_SIZE` is allocated by every copy, so a
  ceiling that measured only the payload admitted 64 bytes per stream
  more than it agreed to. Counted now, and the omit policy removes
  payload *and* padding together, which makes the ceilings exact.

### Fixed — the wholesale codec-parameter copy is gone

- **`avcodec_parameters_copy` no longer runs on attacker data
  anywhere.** It deep-copies every heap field an `AVCodecParameters`
  has, and it did so before anything asked how big they were. Two
  rounds patched the field that had been noticed; the third found
  another — `coded_side_data`, where a MOV `prof` atom puts an **ICC
  profile**, on an ordinary video track that no attachment-shaped check
  ever looked at. Three findings of one class is not three bugs, so the
  copy itself was replaced rather than guarded again.

  **The inventory it was replaced against** (FFmpeg n9.0, 33 fields, 3
  of them heap): `extradata`; `coded_side_data`, meaning the descriptor
  array *and* each entry's payload; and `ch_layout`'s custom channel
  map. Everything else is scalar.

  `extras::bounded_clone_parameters` enumerates those three **by hand**.
  Scalars travel by a bytewise struct copy — so a scalar field a future
  FFmpeg adds is carried for free — and the three pointer seats are
  then nulled on the destination and rebuilt one at a time, each
  measured before it is copied and the whole footprint admitted against
  a budget first. A heap field nobody enumerated is a field that is
  **not copied**, which loses data rather than allocating it, and a
  compile-time size tripwire on `AVCodecParameters` fires when the
  struct changes shape so the omission cannot go unnoticed.

  The rule is stated where the clone lives: *no code path in this crate
  hands attacker-sized parameter data to a wholesale FFI copy.* Every
  parameter copy in the crate now goes through this one function,
  including `TrackExtra::clone_parameters` and `try_clone`, which are
  judged against the footprint their row was admitted at.

  Decode-capability parity is the oracle and it holds with **zero
  assertion edits**: `extradata` keeps its `AV_INPUT_BUFFER_PADDING_SIZE`
  trailing zeroes, every `coded_side_data` entry keeps its type and
  payload, and `ch_layout` is copied through `av_channel_layout_copy`  the one FFmpeg call left on the path, over a single field whose size
  was measured first. The decode, resample and image suites pass
  unchanged.

- **The preflight now sees every stream, not every attachment.**
  `admit_streams` (was `admit_attachments`) measures each stream's
  parameter footprint — allocation-free, checked arithmetic — and
  charges it against two new seats before the track loop clones
  anything.

  | seat | default | why |
  |---|---|---|
  | `DemuxLimits::max_codec_parameter_bytes` | 16 MiB | SPS/PPS is tens of bytes and FLAC headers are kilobytes; what sets the ceiling is the ICC profile in `coded_side_data`, legitimately 0.5–2 MB for a camera or display profile and up to ~10 MB for a device link. |
  | `DemuxLimits::max_total_codec_parameter_bytes` | 64 MiB | Four tracks with a large profile apiece is the realistic high end; nothing in the per-stream ceiling bounds a two-hundred-stream table. |

  New arms: `DemuxError::ParametersTooLarge` and
  `DemuxError::ParametersBudgetExhausted`, separate for the same reason
  the attachment pair are.

- **Residency accounting closed the undercharge.** A synthesized
  attachment's `extradata` is charged to the *attachment* budget, where
  the carrier holds it, and left out of the *parameter* budget, because
  the clone strips it — one set of bytes, one charge. Cover art is
  charged its parked packet to the attachment budget and its retained
  parameter heap (including any `coded_side_data`) to the parameter
  budget. `TrackExtra::parameter_bytes()` reports what a row actually
  retains.

### Fixed — two sibling paths walked around the ceilings

- **An attachment's payload was copied before its budget was charged.**
  `build_tracks` deep-copies each stream's `AVCodecParameters` on its
  way to a `TrackExtra`, and for an `AVMEDIA_TYPE_ATTACHMENT` stream
  **the extradata inside those parameters is the attachment's payload**
  (measured: a 27-byte font fixture reports `extradata_size=27`). So the
  loop paid for the payload — a full `avcodec_parameters_copy` — one
  statement before asking whether it was allowed to. A file declaring a
  gigabyte of "font" allocated the gigabyte and then reported that a
  gigabyte was too much.

  The fix is not a check moved a few lines earlier, because any
  per-track interleaving of judging and paying has the same shape: the
  aggregate budget is only knowable once every track has been *seen*.
  The new `admit_attachments` pass judges the whole file first, reading
  two integers per stream and allocating nothing, and only a container
  that passes in full reaches the loop that builds carriers and
  parameter copies.

- **An accepted attachment retained two copies of itself.** The
  synthesized road kept the payload in its carrier *and* in the
  parameter copy's extradata, behind a budget that counted one of them.
  Censused before it was changed: nothing can use that extradata —
  libavcodec has no decoder for a font, so no road opens a codec
  context from those parameters, and the payload reaches a consumer as
  the attachment packet, which is the delivery the demux tier promises.
  `build_tracks` now strips it. Cover art keeps its extradata: there the
  payload is the parked `AVPacket`, the extradata is not a copy of it, a
  still codec can legitimately need it (MJPEG with an external Huffman
  table), and the measurement says a cover-art stream carries none
  anyway.

  What the budget charges is now **residency**, not payload: a hoisted
  cover-art track is charged its parked packet *plus* the extradata it
  retains, a synthesized one only its carrier. That is what makes the
  budget an honest statement about memory rather than about file
  structure.

- **The resampler's output had no byte ceiling.** `output_capacity`
  bounds the output *sample count*, and only at `i32::MAX` — the
  structural limit of `av_frame_get_buffer`. Nothing bounded the bytes,
  and the two are related by a ratio the caller does not control: the
  capacity is `input_samples × target_rate / source_rate`, and a source
  spec read off an untrusted container can say 1 Hz. 1000 samples at a
  declared 1 Hz against a 48 kHz target is 48,000,001 output samples —
  comfortably inside the existing guard — and 384 MB of `f32` stereo,
  allocated twice counting the carrier copy, from an 8 KB input.

  `FfmpegResampler::new` now takes `FrameLimits` and
  `check_output_bytes` refuses over-budget conversions **before**
  `av_frame_get_buffer` and before any session state moves, with a new
  `ResampleError::OutputTooLarge`. `FrameLimits` rather than a seat of
  the resampler's own: the quantity is bytes of one produced audio
  frame, which is what `max_frame_bytes` means everywhere else, and one
  number for "what a frame may cost" beats a second vocabulary.

- **Terminal closure for the class.** Every `FfmpegBytes` construction
  site in the crate is now enumerated in `buffer.rs`'s module docs,
  each row naming what bounds it — a `limits` seat for a size that comes
  from a file, a format constant for a size that is a property of the
  format, and structurally-zero for the placeholder sites. The table
  lives beside the constructors so a new exit has to answer the question
  the existing ones already answered. Both rounds' findings were rows
  nobody had written down, not exceptions to a rule.

### Changed — the padded-plane copy allocates once

- A plane whose stride is padded is now written **row by row straight
  into the carrier's allocation** (`Arc::new_uninit_slice`, stabilised
  in Rust 1.82 and well under this workspace's 1.95 MSRV), through a
  new crate-internal `FfmpegBytes::from_rows`. The previous spelling
  staged the plane in a `Vec` and then copied it into an `Arc`:
  **two allocations and two copies**, so a 250 MiB frame peaked at
  750 MiB counting FFmpeg's own. It now peaks at the unavoidable 2×.

  The staging `Vec`'s `try_reserve_exact` went with it; the ceiling
  above took its place, and refuses earlier and by name. The side-data
  collectors keep their staging `Vec`, because side data is capped at
  256 KiB and a reportable first allocation is worth the doubling
  there; the plane paths are not small and are not.

### Fixed — the orientation escape lost the words that made it unnameable

- `ImageOrientation::Other` carries **all nine** display-matrix words,
  not the four of the linear part, and a matrix is read as a *named*
  orientation only when the other five are canonical — no translation
  in `x`/`y`, no perspective in `u`/`v`, and unity (`1 << 30`, 2.30
  fixed point per `libavutil/display.h`) in `w`.

  As shipped a moment ago it matched on the linear four alone, so a
  matrix that turned the picture **and** shifted or projected it was
  read as the plain turn and the extra words were dropped. That is
  precisely the collapse this crate's escape-carries-never-collapses
  law exists to prevent, one level in from where the law is usually
  applied. `linear()` remains, now documented as the four-word
  *projection* it always was, and a new `matrix()` answers the whole
  nine — reconstructing the canonical form for a named variant and
  returning what it was handed, unchanged, for the escape.

### Notes

- **The copy is infallible where the view was fallible.** `Arc<[u8]>`
  has no fallible constructor on stable Rust, so an exhausted
  allocator aborts at a copy site rather than returning
  `ConvertError::BufferAcquireFailed`. Everything that bounds *how
  much* can be asked for is unchanged and still runs first — the
  side-data entry and byte caps, the plane-geometry checks, the
  `find_backing_buffer` extent proof — so a hostile stream cannot
  reach that abort by demanding memory. Where a staging `Vec` was
  already in the path (the padded-plane copy, the side-data payload
  copies) its `try_reserve_exact` is kept, so the large allocation
  stays a named refusal and only the carrier copy that follows,
  asked for a size the allocator has just granted, is infallible.

- **The resampler's ordering law is intact.** `prepare_output` used to
  acquire one `AVBufferRef` view per output plane before
  `swr_convert_frame` ran, because wrapping a plane could fail and
  nothing fallible may run after the conversion has consumed input.
  The views are gone; the *proof* is not — every plane pointer is
  still checked non-null and checked to address `plane_len` bytes
  inside one of the frame's own buffers on the near side, so
  `finish_output` copies without anything left to judge and remains
  infallible.

## [0.8.0] - 2026-08-24

### Added

- **`FfmpegDemuxer` implements `Demuxer::take_tracks`**, the
  owned-tracks door `mediadecode`'s demux tier gained in place of a
  `TrackInfo` / `TrackParams` `Clone` — see
  [`mediadecode`'s CHANGELOG]../mediadecode/CHANGELOG.md for the
  message-carrier law behind that redirect. The implementation is
  `mem::take` on the session's own `Vec<TrackInfo<Ffmpeg>>`, built once
  at `open` and untouched until a caller takes it.

  `TrackExtra` does not gain `Clone` either, for the same law. An
  interim version of this entry hand-wrote one through the existing
  checked `try_clone`, to satisfy the same channel bound, and it came
  back out — a full `avcodec_parameters_copy` is not the refcount bump
  a message's `Clone` is required to be. `try_clone` and
  `clone_parameters` are unaffected: they remain the checked copy this
  type offers the one caller that genuinely wants an owned duplicate,
  and `Default` remains absent for the reason it always was — there is
  no checked substitute for `Parameters::default()` to route through.

### Changed (BREAKING)

- **Every struct-shaped enum variant across this crate's errors is now
  a tuple variant wrapping a named payload struct**, following
  `mediadecode`'s own `TrackParams` / `DemuxedPacket` /
  `SubtitlePayload` sweep and the shape this crate's own `Error`
  (`HwDeviceInitFailed`, `AllBackendsFailed`, `FallbackFailed`) and
  `FrameError` already used. A struct variant has no nameable type of
  its own to return from `is_<variant>` / `unwrap_<variant>` /
  `try_unwrap_<variant>`, and traps its fields instead of giving them a
  reusable, documented, accessor-bearing home.

  - **`ResampleError`** — all twelve struct arms (`SourceChanged`,
    `PlaneCount`, `SampleCount`, `UnsupportedRate`,
    `UnsupportedFormat`, `UnsupportedLayout`, `TooManyPlanes`,
    `TimestampOutOfRange`, `ChannelDropped`, `RematrixUnsupported`,
    `TimestampOverflow`, `OutputBuffer`) move to `Variant(Payload)`.
    `Again`, `AfterEof`, `Resample(Error)` and `QueueAlloc` are
    unchanged (already unit or newtype).
  - **`PacketBufferError`** — all eight arms (`Refcount`, `Bounds`,
    `SideDataEntries`, `SideDataArray`, `SideDataPayload`,
    `SideDataBytes`, `UnrepresentableFlags`, `SideDataAlloc`) move the
    same way; every payload keeps the enum's own `Copy + Clone + Debug
    + PartialEq + Eq`.
  - **`DemuxError`**`AttachmentAlloc`, `ParametersMissing`,
    `ParametersAlloc`, `ParametersCopy`, `PacketBuffer`, `ReaderPanic`
    move; `Ffmpeg(#[from] ffmpeg_next::Error)` is unchanged.
  - **`boundary::PacketBuildError`**`UnknownSideData` and
    `SideDataAlloc` move. This crate already had *two* same-named
    `SideDataAlloc` payloads in scope at this file (the read-side one
    on `PacketBufferError`, imported here, and this write-side one,
    native to this module) — the import is aliased
    `SideDataAlloc as BufferSideDataAlloc`; the native struct keeps the
    bare name.
  - **`convert::ConvertError`**`UnsupportedPixelFormat`,
    `InvalidPlaneLayout`, `BufferAcquireFailed` move (found during the
    sweep, not in the original census — this enum already violated the
    same rule). This enum hand-writes `Display` rather than using
    `thiserror`; the extracted payloads keep that idiom (their own
    `impl Display`), and the enum's `Display` delegates to it per
    variant.
  - **`video::VideoDecodeError::PostCommitNeverResynced`** — also
    found during the sweep — moves to
    `PostCommitNeverResynced(PostCommitNeverResynced)`. `Decode` and
    `Convert` were already newtype variants.

  Every extracted payload is `thiserror`-derived where the enum already
  was (`#[error(transparent)]` on the wrapper, `#[from]`, the original
  message moved verbatim onto the payload's own `#[error("...")]`) or
  hand-`Display` where the enum already was (`ConvertError`) — no
  crate's error idiom changed, only the variant shape. A match that
  used to destructure fields now binds the payload and reads it through
  accessors, e.g. `Err(ResampleError::PlaneCount { expected, found })`
  becomes `Err(ResampleError::PlaneCount(p))` with `p.expected()` /
  `p.found()`.

  **The accessor face rides the reshape, same as `mediadecode`'s.**
  `ResampleError`, `PacketBufferError`, `DemuxError`,
  `boundary::PacketBuildError`, `convert::ConvertError`,
  `video::VideoDecodeError`, `audio::AudioDecodeError`,
  `subtitle::SubtitleDecodeError`, and `error::Error` — the crate's
  full error taxonomy, not just the arms the reshape sweep touched —
  now derive `derive_more::{IsVariant, Unwrap, TryUnwrap}`, with
  `#[unwrap(ref, ref_mut)]` and `#[try_unwrap(ref, ref_mut)]` — every
  arm answers `is_<variant>()`,
  `unwrap_<variant>()` / `_ref()` / `_mut()`, and
  `try_unwrap_<variant>()` / `_ref()` / `_mut()`. `Backend`,
  `PictureType`, `resampler::SpecEnd`, and `error::FallbackOrigin` gain
  `IsVariant` too; `FallbackOrigin`'s hand-written `is_post_commit()`
  and `ResampleError`'s hand-written `is_again()` are gone — the
  derive answers both now, `&self`-receiver in place of the old
  by-value one (transparent at every call site, all of which already
  used method-call syntax). This crate did not carry a `derive_more`
  dependency before this entry; it does now, with the `is_variant` /
  `unwrap` / `try_unwrap` features (no `display` — this crate's error
  types keep `thiserror` / hand-`Display`, unchanged).

## [0.7.0] - 2026-08-23

### Added

- **`FfmpegDemuxer``mediadecode`'s demux tier over `libavformat`.**
  Opens from a path (`open`) or from any `Read + Seek` byte source
  through a custom `AVIOContext` (`open_reader`). `Seek` is mandatory
  and not negotiable: MP4 routinely puts `moov` at the end, so a reader
  that cannot go backwards cannot be probed at all — and the face's seek
  law would be unimplementable.

  The track table is built once at open, in stream order, so
  `TrackIndex(i)` and `AVStream.index` are the same number by
  construction. Each row carries the stream's `Parameters`, which is
  what opens a decoder for it — a deep copy with no tie back to the
  format context, so a decoder outlives the demuxer that named it.

  **Three normalizations happen here, and they are all about
  attachments.** libavformat presents cover art as a *video* stream
  carrying `AV_DISPOSITION_ATTACHED_PIC`; this layer maps it to
  `TrackKind::Attachment`, so the `Video` arm is true motion video and
  nothing else. A font's bytes never appear in the packet stream at all
  — an `AVMEDIA_TYPE_ATTACHMENT` stream produces no packets and the
  payload lives in codec extradata — so the packet is synthesized at
  open. And cover art's real packet, which libavformat parks in
  `AVStream.attached_pic` *and* some demuxers also emit, is hoisted at
  open and its duplicate dropped. Every attachment track leaves the open
  with exactly one packet queued — or the open fails — before a single
  `av_read_frame` has run, which is what makes "exactly one packet,
  before any timed packet" a property of the construction rather than a
  promise the pull loop has to keep. A stream that declares cover art
  and parks no payload (a state this build's demuxers do not produce:
  `ff_add_attached_pic` sets the disposition and fills the packet in one
  call) still gets its one packet, empty and marked `synthesized`,
  rather than a place in a queue of packets that may never arrive.

  `seek` converts the target to `AV_TIME_BASE` units and seeks over the
  window `[i64::MIN, target]` — FFmpeg's backward convention, landing on
  the nearest keyframe at or before the target. It clears only the EOF
  latch this layer set itself, leaving a genuine sticky I/O error
  intact, and does not touch the attachment bookkeeping: an attachment
  already handed out is never handed out again, and one not yet handed
  out is still owed.

  A track whose kind is `Unknown` has no delivery arm and its packets
  are not delivered; a corrupt packet is skipped and the read resumed,
  since `AVERROR_INVALIDDATA` is not latched into the `AVIOContext`.

  **A panicking reader is an error, not an abort.** libavformat drives
  an `open_reader` byte source from `extern "C"` callbacks, where a
  panic cannot unwind and terminates the process. Every call into the
  caller's reader runs under `catch_unwind`: the panic is latched,
  reported to C as an ordinary I/O error, and surfaced as
  `DemuxError::ReaderPanic` with the panic's own message from the next
  `open_reader`, `next_packet` or `seek`. The session is terminal from
  there. The caught payload is described and then deliberately
  forgotten: `panic_any` takes any `Send` value and safe code can give
  it a `Drop` that panics, and dropping it after the catch would send
  that second panic out of `read`/`seek` and into the `extern "C"`
  callback — the abort this guard exists to prevent, reached through the
  guard itself (measured: SIGABRT). Leaking one value on a path that has
  already made the session terminal is the cheaper half of that trade by
  a distance.

  **Container metadata is read as bytes, not trusted as text.**
  `ffmpeg_next`'s `DictionaryRef::get` builds its `&str` with
  `from_utf8_unchecked`, and FFmpeg does not validate demuxed metadata
  as UTF-8; a track's `filename` / `mimetype` therefore go through
  `av_dict_get` and a bounded walk, with invalid bytes replaced rather
  than trusted.

- **Boundary helpers that carry a timebase.**
  `video_packet_from_ffmpeg_in`, `audio_packet_from_ffmpeg_in`,
  `subtitle_packet_from_ffmpeg_in` and `data_packet_from_ffmpeg_in` take
  the stream's timebase and stamp it onto every timestamp. An
  `AVPacket`'s integers are ticks in a timebase the packet does not
  carry, so the existing four-argument-less helpers stamp `1/1` and
  leave the caller to remember what the ticks meant; a demuxer holds the
  track table and has no reason to forget. The originals are unchanged
  and now delegate. `attachment_packet_from_ffmpeg` joins them for
  cover-art payloads, which have no timestamps to carry.

- **`FfmpegResampler``mediadecode`'s resample seam over
  `swresample`.** `FfmpegResampler::new(source, target)` takes both
  specs as [`ResampleSpec`]s and neither is inferred. The source is read
  off a demuxed track (`ResampleSpec::from_parameters`, the
  "source from `TrackInfo`" path) or off the opened decoder
  (`ResampleSpec::from_decoder`); the target is the caller's options.

  **Output timestamps are counted, not computed.** The timeline is
  anchored on the first *input* timestamp and advanced by the number of
  samples actually produced, so no arithmetic depends on how many
  samples a given `swr_convert_frame` happened to yield and the frames
  drained after `send_eof` continue the same line. The tail is real:
  the 44.1 kHz → 16 kHz lane pins that `send_eof` still has a frame to
  give, which is the difference between a file's last tens of
  milliseconds surviving and not.

  **A frame whose rate, sample format or channel layout is not the
  source spec is refused by name** (`ResampleError::SourceChanged`), and
  "nothing ready yet" is `ResampleError::Again` — the same mechanism
  `AudioStreamDecoder::receive_frame` uses, one tier along.
  `send_frame` after `send_eof` is refused too rather than silently
  accepted; `flush` is the way back to a reusable resampler.

  Two things this layer had to learn about FFmpeg to be correct, both
  recorded where they are relied on. A WAV without a
  `WAVE_FORMAT_EXTENSIBLE` channel mask genuinely declares **no**
  layout, and FFmpeg reports that unspecified layout in the codec
  parameters, in the codec context and on every decoded frame —
  substituting a default would make the source spec disagree with the
  frames it describes and refuse all of them. But `swr_init` *does*
  substitute a default internally, and then compares every frame handed
  to it against that one — so the frames this layer stages carry the
  post-init layout while the mid-stream check keeps comparing against
  the declared one. Custom and ambisonic layouts are refused outright:
  both keep a heap-allocated channel map that a `Copy` layout wrapper
  cannot own safely, and a silent approximation would be worse than a
  refusal. `from_parameters` / `from_decoder` answer `None` for them,
  and — because `ResampleSpec::new` is `const` and total —
  `FfmpegResampler::new` is the choke point that refuses them by name
  (`ResampleError::UnsupportedLayout`), along with a rate of zero or
  past `c_int`, `AV_SAMPLE_FMT_NONE`, and a planar spec with more
  channels than a frame has plane slots
  (`ResampleError::TooManyPlanes` — planar 22.2 is twenty-four planes
  against the model's eight, unusable as a source because no frame
  could carry it and worse as a target because the failure would land
  after `swr` had consumed the input). Nothing hazardous, and nothing
  unusable, reaches `swr` or a staged `AVFrame` whichever route a spec
  came in by.

  **The pair is checked too, not just each end.** Two individually
  valid layouts can still lose whole channels between them: `swr` mixes
  the channel positions its rematrix table knows and processes the rest
  of the input as though it were absent. Measured against the linked
  FFmpeg 9 with a tone isolated in each source channel, packed
  22.2 → mono drops fifteen of twenty-four and `cube` → stereo drops two
  of *eight* — so channel count is not the rule. The rule is asked of
  FFmpeg: `swr_build_matrix2` builds the matrix it would use, and a pair
  where any source channel reaches no output is refused
  (`ResampleError::ChannelDropped`), as is one FFmpeg will not matrix at
  all (`ResampleError::RematrixUnsupported`). LFE's mix level is forced
  non-zero for the question, so FFmpeg's deliberate downmix policy —
  which leaves LFE out — is not mistaken for a channel it cannot carry.
  Accepting such a conversion knowingly needs an explicit mix-matrix
  seat on the spec; until that exists, refusal is the honest answer.

  The pair judged is the **effective** one — the layouts `swr` is
  configured with, resolved before the check rather than after it. An
  unspecified layout becomes FFmpeg's default for its channel count, and
  twenty-four unspecified channels resolve to exactly the 22.2 whose
  explicit conversion is refused, so judging the declared pair left that
  routing a door. This is the second half of the crate's two-layout
  bookkeeping and it composes with the first: the **declared** layout is
  what decoded frames carry and stays the yardstick for the mid-stream
  refusal (*is this frame the stream I was built for?*), while the
  **effective** layout is what `swr`, every staged `AVFrame` and now
  this check use (*what will `swr` actually do?*). A maskless WAV — one
  channel resolving to mono, two to stereo — keeps converting exactly as
  before.

  **Nothing that can fail is left on the far side of the conversion.**
  Every resource a converted frame needs — the output frame, one
  refcounted view per plane, a placeholder for every unused slot, and
  the room in the ready queue — is acquired *before*
  `swr_convert_frame` touches a sample, on the ordinary path and on the
  EOF drain alike; the views are taken at full capacity and trimmed
  afterwards by a shrink that allocates nothing. The step that turns a
  converted frame into a `mediadecode` one is consequently infallible by
  signature, which is what makes the property hold rather than
  hold-for-now: a failure after `swr` has consumed input leaves a
  session no caller can act on, since retrying feeds the same samples
  twice and continuing loses them.

  **State is committed after the work it describes succeeds.** A frame
  refused for its geometry does not anchor or advance the output
  timeline; timestamps are counted with checked arithmetic and an
  overflow is `ResampleError::TimestampOverflow` — raised *before*
  `swr` consumes the frame, checked against the most samples the call
  could produce, so a refused conversion leaves a session the caller can
  still use (the check used to run after the conversion, which left the
  filter's history moved, an output frame built and dropped, and the
  timeline anchored where every later frame overflowed too) — rather
  than `i64::MAX` repeated; the anchor itself is rescaled with the checked rung before
  anything is staged, so a timestamp that cannot land on the output
  timeline is `ResampleError::TimestampOutOfRange` with the resampler
  untouched, rather than a clamp — a positive clamp used to surface only
  after `swr` had consumed the input, and a negative one landed on
  `i64::MIN`, which is `AV_NOPTS_VALUE`, erasing the timestamp instead
  of reporting it; plane geometry is settled in checked arithmetic *before* any
  allocation is sized from a frame header, and every allocation is
  checked (`frame::Audio::new` dereferences `av_frame_alloc` unchecked
  and discards `av_frame_get_buffer`'s return, so this crate allocates
  its audio frames through a local helper that does neither). `flush`
  rebuilds the `swr` context rather than draining it: a drain that gave
  up and a drain that finished are indistinguishable from outside, and a
  reset that reports success must not leave the previous stream's tail
  inside the filter.

- **`resample` feature, on by default.** `FfmpegResampler`,
  `ResampleError`, `ResampleSpec`, `SpecEnd` and the
  `ffmpeg-next/software-resampling` link they need move behind it, so a
  decode-only consumer building `--no-default-features --features std`
  drops `libswresample` from the link line entirely rather than merely
  not calling into it. The core `mediadecode::resampler` trait module
  stays ungated — it adds no dependency of its own, backend or
  otherwise, over what the crate already compiles unconditionally.

- **`SampleFormat::to_ffmpeg` / `SampleFormat::from_ffmpeg`** — the
  direction this newtype was missing. A raw sample-format integer read
  out of a container cannot be cast back into the bindgen enum to reach
  FFmpeg's safe API, so `to_ffmpeg` matches it against compile-time
  constants instead, exactly as `boundary::from_av_pixel_format` comes
  the other way.

- **`DataPacketExtra`, `AttachmentPacketExtra`, `TrackExtra`** — the
  demux tier's `*Extra` carriers, and `impl DemuxAdapter for Ffmpeg`
  binding them. `AttachmentPacketExtra::synthesized` records whether a
  payload came from a real packet or was built out of codec extradata,
  which is the first thing to check when an attachment looks wrong.
  `TrackExtra::disposition` is the raw `AV_DISPOSITION_*` bit set rather
  than `ffmpeg_next`'s `Disposition`, whose `from_bits_truncate` would
  drop bits this build has no constant for.

### Fixed

- **A sparse thumbnail track is a timed track, not one attachment.**
  Classification tested `AV_DISPOSITION_ATTACHED_PIC` alone, and FFmpeg
  documents `AV_DISPOSITION_TIMED_THUMBNAILS` — "the stream is sparse,
  and contains thumbnail images, often corresponding to chapter
  markers" — as *only ever* used together with it. Such a stream was
  therefore read as cover art: the exactly-once attachment contract
  queued the parked copy and the delivery loop dropped every timestamped
  thumbnail after it. The two bits are now read off the raw
  `AVStream.disposition` (`ffmpeg_next`'s `Disposition` mints no
  `TIMED_THUMBNAILS` constant, and its `from_bits_truncate` drops what
  it cannot name — which is how the distinction went missing), and a
  timed-thumbnail stream goes to the **`Video`** arm. That does not
  bend "cover art is an attachment, not video": the reason behind that
  ruling is that a still with no timeline must not look like a motion
  track, and this stream *is* on the timeline — sparse video, with a
  codec id, a frame size and packets that carry timestamps.

- **A packet's side data arrives with the packet, and a side-data-only
  packet is no longer mistaken for an empty marker.** The `*Extra`
  carriers have always documented a `side_data` seat and the conversion
  never filled one; measured on this repository's own generated corpus,
  every container carries at least one packet with real side data
  (`AV_PKT_DATA_SKIP_SAMPLES` — the encoder-delay trim an MP3 or AAC
  stream must be cut by), so that data was dropped at the boundary on
  every file. Worse, a packet with **no body** and only side data —
  FFmpeg's shape for `AV_PKT_DATA_NEW_EXTRADATA` and for a parameter
  change — read as "empty, skip it", so a decoder could be left running
  on parameters the container had already replaced, with nothing said.

  All four timed arms now collect side data — **whole, or not at all**.
  The collection is bounded (64 entries or as many as this build names,
  whichever is larger; 256 KiB; `try_reserve_exact`), and every bound is
  an **error**, not a truncation: a packet whose side data cannot be
  carried complete is refused by name
  (`PacketBufferError::SideDataEntries` / `SideDataBytes` /
  `SideDataAlloc`, surfacing as `DemuxError::PacketBuffer`) rather than
  delivered missing the entries a decoder acts on. Truncating would put
  the original defect back twice over: a body-bearing packet reaching
  the codec without its `NEW_EXTRADATA`, and a side-data-only packet
  losing its only content and vanishing as an empty marker. The arms
  also deliver a side-data-only packet with an owned empty buffer.

  The same rule reaches the pointers, not only the caps: a count is
  judged before the array it describes, so a malformed or over-cap count
  is refused whether or not the array happens to be null
  (`SideDataArray` names the missing array), and an entry declaring
  bytes it does not carry is refused (`SideDataPayload`) rather than
  read as an empty entry that charges the budget nothing. A zero-size
  entry is still a marker and still welcome.
  `SubtitlePacketExtra` and `DataPacketExtra` gained the seat the other
  two already had. `AttachmentPacketExtra` deliberately did not: an
  attachment is its bytes, so a packet carrying none carries no
  attachment, and that arm still answers `Ok(None)`.

  **And the reverse direction reattaches it**, which is what makes the
  capture worth anything: the three `ffmpeg_packet_from_*_packet`
  helpers are what the trait decoders hand to the codec, and they
  rebuilt a packet from body and timestamps alone. Measured end to end
  on `cover.mp3`: with the trim reattached the decoder returns 88 200
  samples — exactly the two seconds the file holds — and without it
  89 856, the encoder's padding included. A side-data type this build of
  FFmpeg does not name is refused
  (`PacketBuildError::UnknownSideData`) rather than dropped or handed to
  C as an invalid discriminant.

- **Every packet flag survives both directions.** Forward conversion
  went through `ffmpeg_next`'s `Packet::flags()`, whose `Flags` bit set
  names `KEY` and `CORRUPT` and truncates the rest away before this
  crate sees them; the reverse rebuilt only those two. `PacketFlags` is
  a bit set whose documented lossless door is `from_bits_retain`, so
  both directions were breaking its contract — and losing
  `AV_PKT_FLAG_DISCARD`, which tells a consumer to decode a packet and
  throw its output away, makes preroll output look like something to
  keep. `AVPacket.flags` is now read and written as the raw integer it
  is, so `DISCARD`, `TRUSTED`, `DISPOSABLE` and the bits nothing names
  yet all round-trip. A compile-time assertion states that every flag
  this build names fits the byte `PacketFlags` carries; a packet
  carrying one that does not is refused
  (`PacketBufferError::UnrepresentableFlags`) rather than delivered a
  bit short. The hoisted cover-art packet reads its flags through the
  same reader: it is built by hand from `AVStream.attached_pic` rather
  than through the boundary conversion, and it used to be built with
  none at all — FFmpeg marks an attached picture `AV_PKT_FLAG_KEY`, so
  every cover this crate delivered arrived saying it was not a
  keyframe. A synthesized attachment still carries no flags, because
  no packet was parked to read them from.

- **`TrackExtra` no longer derives `Clone` or `Default`, and the
  decoder handoff is checked.** Both derives went through
  `ffmpeg_next`'s `Clone` / `Default` for `Parameters`, so copying a
  track row from safe public code reached the same unchecked
  allocation — measured, a SIGSEGV — and the documented handoff was
  `parameters().clone()`, which is that same clone. `Clone` cannot
  report a failure, so the type does not implement it:
  `TrackExtra::try_clone` is the row copy with an answer, and
  `TrackExtra::clone_parameters` is the handoff that opens a decoder.
  `parameters()` still lends the parameters for inspection —
  `ResampleSpec::from_parameters` reads them — but nothing in this crate
  asks a caller to clone them unchecked any more.

  **And a `TrackExtra` cannot exist over parameters that were never
  allocated.** `Parameters::new()` / `Default` hand back a null-backed
  value when `avcodec_parameters_alloc` failed and report nothing, so
  checking only the destination of a copy left the source trusted: once
  the allocator recovered, `avcodec_parameters_copy(out, NULL)`
  dereferenced null from safe public code. `TrackExtra::new` is
  therefore fallible and refuses one
  (`DemuxError::ParametersMissing`), which gives the type a non-null
  invariant from birth, and the copier checks its source as well —
  belt and braces, because the invariant is a promise and the check is
  a fact. `ResampleSpec::from_parameters` and `from_decoder` grew the
  same guard: the first used to ask `medium()` on the way in, which
  dereferences the pointer inside ffmpeg-next before any code of this
  crate runs.

- **A track's codec parameters are copied with both fallible steps
  checked.** `ffmpeg_next`'s `Clone` for `Parameters` checks neither:
  `Parameters::new` does not test `avcodec_parameters_alloc` for null
  and `clone_from` dereferences it immediately — measured, that is a
  SIGSEGV under a failed allocation — while the copy's return value is
  discarded, so a partial copy (a failed extradata allocation, say)
  produced parameters that look complete and open a decoder wrong.
  Every track in the table goes through this, so it is a whole session
  built on parameters that are not the file's. A local helper checks
  both legs and reports `DemuxError::ParametersAlloc` /
  `ParametersCopy`; a partial copy is freed on the way out.

### Changed (BREAKING)

- **Wrapping a packet's payload answers `Result<Option<_>>`.**
  `FfmpegBuffer::from_packet`, `video_packet_from_ffmpeg`,
  `audio_packet_from_ffmpeg`, `subtitle_packet_from_ffmpeg`, the four
  `*_packet_from_ffmpeg_in` variants and `attachment_packet_from_ffmpeg`
  now return `Result<Option<_>, PacketBufferError>`.

  `Ok(None)` is a packet that carries no payload — the empty marker some
  demuxers emit, and the only thing a pull loop may skip. An `Err` is a
  payload that *is* there and could not be carried: `av_buffer_ref`
  refusing a reference under memory pressure, or a packet whose payload
  does not lie inside its own buffer. The single `None` these used to
  share made the second look like the first, so a demuxer under memory
  pressure dropped real compressed bytes and read on as though the file
  had said so. `FfmpegDemuxer::next_packet` surfaces the failure as
  `DemuxError::PacketBuffer`, naming the stream.

  Call sites that skipped `None` add one `?` or an `expect`; nothing
  else changes.

- **The three `ffmpeg_packet_from_*_packet` helpers answer
  `Result<Packet, PacketBuildError>`.** They can now fail for a reason
  `ffmpeg_next::Error` cannot spell: a side-data entry whose type this
  build of FFmpeg does not name, or whose allocation failed.
  `Error::PacketBuild` carries it into the decoder error types.

## [0.6.0] - 2026-08-21

### Added

- **A layout that falls off the constant table now gets one more
  chance: FFmpeg's own name for it.** `channel_layout_from_ffmpeg`
  keeps its constant-arm table as the first and authoritative rung —
  a mapped constant is answered there, byte for byte, and renders
  nothing and parses nothing. Only on a miss does the adapter ask
  `av_channel_layout_describe` what FFmpeg calls the layout and read
  that word through `mediaframe::audio::ChannelLayout`'s `FromStr`,
  the total door the vocabulary already exposes.

  A named variant wins; anything the vocabulary does not name still
  lands on the absent sentinel with FFmpeg's rendering in `text()`.
  The rendering never rides `known_kind`'s `Other` escape, so
  "unrecognised" stays one value rather than one per spelling.

  What the rung reaches that the table cannot: `binaural`, `5.1.2`
  and `9.1.6` — all three named by `ChannelLayout`, none of them
  minted as an `ffmpeg_next` constant — plus FFmpeg 9's own `5.1.4`,
  whose side-surround mask `ffmpeg_sys_next`'s bundled
  `channel_layout_fixed.h` misses, its
  `AV_CH_LAYOUT_5POINT1POINT4_BACK` still carrying FFmpeg 8's
  back-surround formula. And, from here on, any layout a later
  FFmpeg adds that the vocabulary already names, with no edit in
  this crate: FFmpeg speaks the name, the vocabulary reads the word,
  one source.

  On the `ChannelLayoutDescription` path the layout is described
  once and that single rendering feeds both `known_kind` and
  `text`, so the two can never answer from different FFmpeg calls.

### Changed (BREAKING)

- **`channel_layout`'s conversions produce `mediaframe::audio` types,
  and are named after them.** `mediadecode::channel` is gone (see
  [`mediadecode`]../mediadecode/CHANGELOG.md#unreleased); the
  vocabulary lives upstream now, and the `Kind` ornament is buried with
  the enum it named.

  | was | is | returns |
  | --- | -- | ------- |
  | `channel_layout_kind_from_ffmpeg` | `channel_layout_from_ffmpeg` | `ChannelLayout` |
  | `audio_channel_order_kind_from_ffmpeg` | `channel_order_from_ffmpeg` | `ChannelOrder` |
  | `audio_channel_order_kind_from_raw` | `channel_order_from_raw` | `ChannelOrder` |
  | `audio_channel_layout_from_ffmpeg` | `channel_layout_description_from_ffmpeg` | `ChannelLayoutDescription` |
  | `audio_channel_layout_from_raw_ptr` | `channel_layout_description_from_raw_ptr` | `ChannelLayoutDescription` |

  The bodies are unchanged: same raw-pointer discipline (`order` is
  read as `i32` before any `AVChannelOrder` value is formed), same
  union guards, same `av_channel_layout_describe` buffer growth, same
  UTF-8-lossy label decoding. Only the target vocabulary moves.
  FFmpeg's own `ChannelLayout` is imported as `AvChannelLayout` inside
  the module so the plain name belongs to what the functions produce.

- **`AudioAdapter::ChannelLayout` and the `AudioFrame` alias bind
  `mediaframe::audio::ChannelLayoutDescription`.** A caller that stored
  a decoded layout in a type of its own re-spells that type; a caller
  that only passed frames along needs nothing. The layout's *name* now
  arrives as `ChannelLayout` rather than `ChannelLayoutKind`, the
  absent case is `ChannelLayout::default()` (the `Other("")` sentinel)
  rather than `ChannelLayoutKind::Unknown`, and FFmpeg's rendering is
  read back with `text()` where it was `description()`.

- **Three layout arms answer to different idents, and one is gone.**
  Upstream's idents follow FFmpeg's constants except where a constant
  names an arrangement no reader would recognise, so `SURROUND` is
  `Ch3_0`, `AV_CH_LAYOUT_2_1` is `Ch3_0Back` and `AV_CH_LAYOUT_2_2` is
  `QuadSide`. The `_7POINT1_TOP_BACK` arm is deleted outright:
  `ffmpeg-next` defines that constant as an alias of
  `AV_CH_LAYOUT_5POINT1POINT2_BACK`, which this table already matches
  twelve arms earlier, so the arm could never be reached and the
  variant it produced does not exist upstream.

  Layouts the table still cannot name — `BINAURAL`, `_5POINT1POINT2`
  (the side one) and `_9POINT1POINT6`, all three named by
  `mediaframe::audio::ChannelLayout` — have no `ffmpeg_next`
  `ChannelLayout` constant to match against. The describe rung above
  reaches them all the same; without it they would keep arriving as
  the absent sentinel with FFmpeg's spelling preserved in `text()`,
  exactly as before.

- **`mediaframe` is a direct dependency**, pinned with `alloc`: the
  audio household is compiled only at the alloc-or-std tier.

## [0.5.0]

Tracks `mediadecode` 0.5.0, which crosses `mediaframe` 0.4 → 0.5 — a
breaking minor. `mediaframe` is a public dependency of the core crate
and its `PixelFormat` and `color` types appear in this adapter's own
signatures (`convert`, `pixdesc`, `frame`), so this adapter's
re-exported surface moves with it. See
[`mediadecode` 0.5.0](../mediadecode/CHANGELOG.md#050).

Nothing about the FFmpeg boundary's behaviour changes, and no source
line moved. The version bump is the public-dependency crossing, not a
change of conduct: `mediaframe` 0.5 adds no pixel-format or colour
variant, so this crate's exhaustive maps over `PixelFormat` — which
would have been `E0004` had one appeared — are unchanged, and the
FFmpeg constant tables still round-trip.

Two upstream conveniences arrive for free: the re-exported vocabularies
carry `ROSTER`, and `BayerPattern` is closed, so a downstream matching
it can drop its wildcard arm.

## [0.4.0] - 2026-08-19

### Changed (BREAKING)

- **`convert::ConvertError::UnsupportedPixelFormat` names the format
  again.** It was `UnsupportedPixelFormat(PixelFormat)`; it is now a
  struct variant carrying `format`, `raw` and `name`. The `format`
  field is the old payload unchanged — still `PixelFormat::None` for
  the fall-through, because this restores the *diagnostic*, not the
  `Unknown(u32)` variant mediaframe 0.3 struck. What is restored is
  everything the message lost with it:
  - `raw: i32` — the `AVFrame.format` integer exactly as FFmpeg wrote
    it, present unconditionally.
  - `name: Option<SmolStr>` — FFmpeg's own name for that integer, from
    `av_get_pix_fmt_name`, or `None` when libavutil has no descriptor
    for it.

  The rendered message goes from `unsupported pixel format None` back
  to `unsupported pixel format None (AVPixelFormat <n> =
  "videotoolbox_vld")`, and to `… (AVPixelFormat 99999, unnamed by
  libavutil)` where there is no name. This supersedes the note under
  0.4.0 below, which recorded the message losing the raw integer.

  Callers matching `ConvertError::UnsupportedPixelFormat(pf)` move to
  `ConvertError::UnsupportedPixelFormat { format, .. }`.

  The lookup does not weaken the crate's FFI stance. `av_get_pix_fmt_name`
  is redeclared with a plain `c_int` parameter rather than the bindgen
  enum, so an integer outside our build's discriminant set is never
  turned into an `AVPixelFormat` — which is the whole reason the
  fall-through exists in the first place. Two tests pin that libavutil
  answers such integers with null rather than misbehaving.

Tracks `mediadecode` 0.4.0, which crosses `mediatime` 0.1 → 0.3 and
`mediaframe` 0.1 → 0.3 — two breaking minors each. Both are public
dependencies of the core crate, so this adapter's re-exported
signatures move with them. See
[`mediadecode` 0.4.0](../mediadecode/CHANGELOG.md#040). This release
also crosses `ffmpeg-next` 8.1 → 9. Nothing about the FFmpeg
boundary's *behaviour* changes: the same raw integers are accepted,
the same formats are deliverable, and the same frames are rejected.

### Changed (BREAKING)

- **`ffmpeg-next` 8.1 → 9**, tracking FFmpeg 9. `ffmpeg_next::Packet`,
  `Frame`, `Error`, `decoder::Audio` and `decoder::Subtitle` appear in
  this crate's public signatures, so `ffmpeg-next` is a public
  dependency and downstream crates must move to the same major.
  No adapter source changed: the bump is spelling-clean, and the
  boundary's behaviour is unaffected. What it buys is the
  `ffmpeg_9_0` code path — `ffmpeg-sys-next` 8.1's version table
  topped out at `ffmpeg_8_1` and gated 9.0 off even when linked
  against a 9.x system library. Two surfaces moved in FFmpeg 9 but do
  not reach this crate: capability queries (`Audio`/`Video`'s
  `rates` / `formats` / `channel_layouts`) now read
  `avcodec_get_supported_config` instead of the codec struct's
  fields, and none of them are called here; and the codec-id
  vocabulary dropped `V308` / `V408` / `V410` while adding
  `WEBP_ANIM` / `APPLE_APAC`, which `CodecId` absorbs as a
  `#[repr(transparent)]` `i32` with a fall-through `Debug` arm. The
  pixel formats this crate already mapped to `None` for want of an
  `AV_PIX_FMT_*` constant (`V210`, `V410Le`, `Yuva420p12Le`,
  `Yuva444p14Le`) are still absent in 9, so their fall-through
  stands.
- **The pixel-format fall-through is now `PixelFormat::None`.**
  `boundary::from_av_pixel_format` returned
  `PixelFormat::Unknown(raw as u32)` for a raw `AVFrame.format`
  integer with no mapping; mediaframe 0.3 struck that variant, so the
  raw integer no longer rides along in the returned value.
  `PixelFormat::None` is a *named* member of the vocabulary (FFmpeg's
  own `AV_PIX_FMT_NONE`, and the `Default`), which is also now the
  exact answer for `AV_PIX_FMT_NONE` itself. Callers matching
  `PixelFormat::Unknown(_)` switch to `PixelFormat::None`; callers
  that read the payload have the raw integer at the call site, where
  `is_hardware_pix_fmt` already reads it. Rejection is unchanged —
  `pixdesc::to_av_pixel_format`, `pixdesc::is_deliverable`,
  `frame::is_supported_cpu_pix_fmt` and both geometry tables refuse
  the fall-through exactly as before. `boundary::empty_video_frame` /
  `try_empty_video_frame` fill the same placeholder.
- **`Timebase` construction is signed.** `mediatime` 0.2 made
  `Timebase`'s numerator and denominator `i32` / `NonZeroI32`, so the
  README example, `examples/decode_via_trait.rs`, and the three
  integration tests drop their `as u32` casts on
  `ffmpeg::Rational::numerator()` /
  `.denominator()` — which are C `int`s to begin with. A stream
  declaring a negative numerator now panics in `Timebase::new`
  instead of wrapping to a huge `u32`.

### Changed

- **`mediadecode` dep**: bumped to `0.4`.
- Crate-internal pixel-format predicates and tables now borrow rather
  than consume, since `mediaframe::PixelFormat` is no longer `Copy`:
  `pixdesc::{is_deliverable, plane_geometry, to_av_pixel_format}`,
  `frame::{is_supported_cpu_pix_fmt, plane_row_bytes_for,
  plane_height_for}` and `convert::{is_yuvj, map_range_for}` take
  `&PixelFormat`. All are `pub(crate)` or private — no public
  signature moves.
- `convert::ConvertError::UnsupportedPixelFormat` still carries the
  format by value; the two construction sites move it in after the
  borrowing checks, so no clone was introduced. Its rendered message
  changes for the fall-through case, from
  `unsupported pixel format Unknown(119)` to
  `unsupported pixel format None` — the raw integer is no longer in
  that log line.

[0.4.0]: https://github.com/findit-studio/mediadecode/releases/tag/mediadecode-ffmpeg-v0.4.0

## [0.3.3] - 2026-06-25

Every byte-addressable CPU pixel format FFmpeg can produce now decodes
and delivers ([#15](https://github.com/findit-studio/mediadecode/pull/15)).
The hand-maintained per-format geometry table is gone from the decode
path; libavutil's own descriptor is the authority.

### Added

- **`pixdesc` module.** Per-plane geometry — visible row bytes and row
  count — is derived from `av_image_fill_linesizes` and
  `av_image_fill_plane_sizes` for the exact `(format, width, height)`,
  so it is correct by construction for any format FFmpeg can describe
  rather than for the formats someone remembered to tabulate. The
  safety stance is unchanged: an `AVPixelFormat` is never constructed
  from a runtime integer, only mapped from a recognised `PixelFormat`
  to a compile-time `AV_PIX_FMT_*` constant.

### Fixed

- **The YUVJ family decodes.** `yuvj420p` / `yuvj422p` / `yuvj440p` /
  `yuvj444p` / `yuvj411p` previously fell through to `Unknown` and were
  rejected at convert; they now map, deliver, and carry their JPEG
  range on `ColorInfo::range`. This is the real-world fix for MJPEG and
  JPEG-range footage.
- **`boundary::from_av_pixel_format` covers 251 formats**, up from 63.

### Changed

- **Deliberate exclusions**, by descriptor flag rather than by omission:
  Bayer mosaics (a demosaic question, not a geometry one), GPU surface
  formats (the hardware path transfers to a CPU format first), paletted,
  monochrome and sub-byte-packed RGB. All are rejected up front by
  `pixdesc::is_deliverable` with a layout the crate can state, instead
  of being read as `linesize × height` bytes of guesswork.
- Four formats have no constant in the linked `ffmpeg-sys-next` 8.1 and
  so still fall through: `V210`, `V410LE`, `YUVA420P12LE`,
  `YUVA444P14LE`.

> Downstream note carried from the PR: this crate now delivers formats
> the colconv resample layer did not yet handle at the time, which
> decode and then fail at resample. That is not a regression — they
> failed earlier, at decode, before this release — and closing the gap
> is colconv-side work.

[0.3.3]: https://github.com/findit-studio/mediadecode/releases/tag/mediadecode-ffmpeg-v0.3.3

## [0.3.2] - 2026-06-24

Hardware decode that collapses **after** the probe has committed now
falls back to software instead of failing the stream
([#13](https://github.com/findit-studio/mediadecode/pull/13)).

### Added

- **`FallbackOrigin`** (`Probe` / `PostCommit`) on the
  `AllBackendsFailed` payload, with `AllBackendsFailed::origin()` and
  `AllBackendsFailed::new_post_commit()`. The wrapper routes its
  software-fallback replay on this explicit signal rather than inferring
  the origin from whether `unconsumed_packets` is empty — both origins
  can be empty (a probe-era failure on the very first packet has no
  history to surface either), so emptiness cannot tell them apart.
  Conflating the two made a probe-era first-packet cap trip look
  post-commit, and the packet could be dropped in silence.

### Fixed

- **Runtime HW failure no longer ends the stream.** When the committed
  backend fails after the probe has collapsed, the decoder opens a
  software decoder cold, forwards the failing call's packet (or EOF),
  and resyncs at the next keyframe — a bounded, logged gap rather than
  a dead stream. Probe-era failures keep the previous behaviour: the
  buffered history is replayed and the current packet routed on.

[0.3.2]: https://github.com/findit-studio/mediadecode/releases/tag/mediadecode-ffmpeg-v0.3.2

## [0.3.1] - 2026-06-14

Additive release ([#10](https://github.com/findit-studio/mediadecode/pull/10)).

### Added

- **`Clone` on the decode-path error types**`Error`,
  `AllBackendsFailed`, `FallbackFailed`, `AudioDecodeError` and
  `ConvertError` — so a consumer can forward one error event to several
  per-stream subscribers. Every payload was already cheaply clonable:
  `ffmpeg_next::Error` is `Copy`, `ffmpeg_next::Packet` is `Clone`, and
  `Backend` is `Copy`. `mediadecode::AudioFrame` gains `Clone` in the
  same release — see
  [`mediadecode` 0.3.1]../mediadecode/CHANGELOG.md#031---2026-06-14.

[0.3.1]: https://github.com/findit-studio/mediadecode/releases/tag/mediadecode-ffmpeg-v0.3.1

## [0.3.0] - 2026-06-07

Tracks `mediadecode` 0.3.0, which flips the shared vocabulary crate from
`videoframe` 0.2 to `mediaframe` 0.1
([#7](https://github.com/findit-studio/mediadecode/pull/7),
[#8](https://github.com/findit-studio/mediadecode/pull/8)). See
[`mediadecode` 0.3.0](../mediadecode/CHANGELOG.md#030---2026-06-07) for
what moved in the vocabulary itself.

### Changed (BREAKING)

- **`mediadecode` dep**: bumped to `0.3`. The re-exported vocabulary
  types are `mediaframe`'s now, so this adapter's type aliases and
  signatures carry the new identity.
- **Two colour-transfer mappings are renamed**, tracking upstream:
  `ColorTransfer::Bt470M``Gamma22` and `Bt470Bg``Gamma28`. The
  FFmpeg wire mapping is untouched — the same `AVCOL_TRC_GAMMA22` /
  `AVCOL_TRC_GAMMA28` land on the same values under their new spelling.

### Changed

- Version bumped to 0.3.0 with the rest of the workspace.

[0.3.0]: https://github.com/findit-studio/mediadecode/releases/tag/mediadecode-ffmpeg-v0.3.0

## [0.2.0] - 2026-05-15

Tracks `mediadecode` 0.2.0. The pixel-vocabulary types
(`PixelFormat`, color enums, frame primitives) now live in the
`videoframe` crate and are re-exported through `mediadecode`; this
release adapts the FFmpeg boundary to the new `PixelFormat::Unknown(u32)`
shape and updates the type aliases the crate re-exports.

### Changed (BREAKING)

- **`PixelFormat::Unknown` shape**: re-exported `PixelFormat` is now
  `Unknown(u32)` (tuple variant) instead of the prior unit variant
  — see [`mediadecode` 0.2.0]../mediadecode/CHANGELOG.md#020---2026-05-15.
- **FFmpeg boundary fallback** now preserves the raw `AVPixelFormat`
  identifier through `PixelFormat::Unknown(raw as u32)` instead of
  collapsing to a bare `Unknown`. Round-trips losslessly via
  `PixelFormat::{from_u32, to_u32}`.
- **Type aliases reshape**: `VideoFrame`, `AudioFrame`,
  `SubtitleFrame`, `VideoPacket`, `AudioPacket`, `SubtitlePacket`
  inherit the upstream `PixelFormat` shape change. Downstream
  callers matching on `Unknown` in destination frames need to
  switch to `Unknown(_)`.
- **`Error` enum variants** are now newtype-tuple form wrapping
  payload structs (matches the convention in
  [`videoframe`]https://crates.io/crates/videoframe). Affected
  variants: `HwDeviceInitFailed`, `AllBackendsFailed`,
  `FallbackFailed`. Pure tuple variants (`Ffmpeg`, `NoCodec`,
  `BackendUnsupportedByCodec`) unchanged. Callers destructuring
  `Err(Error::AllBackendsFailed { attempts, .. })` must switch to
  `Err(Error::AllBackendsFailed(p))` and call `p.attempts()` /
  `p.unconsumed_packets()`. Owning-move paths for the rescued
  packets are preserved via `p.into_unconsumed_packets()` /
  `p.into_parts()`, so non-seekable callers can still relinquish
  the `Vec<Packet>` without cloning. The hand-written `Debug` that
  printed `[N packets]` (because `ffmpeg_next::Packet` has no
  `Debug`) now lives on the payload structs.
  All three new variants also carry `#[from]`, joining `Ffmpeg`
  which already had it — so `impl From<HwDeviceInitFailed> for Error`,
  `impl From<AllBackendsFailed> for Error`, and
  `impl From<FallbackFailed> for Error` are auto-generated, and
  helpers returning `Result<_, HwDeviceInitFailed>` etc. can be
  `?`-propagated into `Result<_, Error>` directly.

### Changed

- **`mediadecode` dep**: bumped to `0.2`.
- Boundary mapping in `pixel_format_from_ffmpeg` and the
  side-data conversion paths updated to the new
  `PixelFormat::Unknown(u32)` shape (17 fallback / assertion /
  default-frame sites across `mediadecode-ffmpeg` and
  `mediadecode-webcodecs`).

### Added

- **`Debug` impl for `Frame`** — manual `core::fmt::Debug` impl
  showing dimensions / format so the only public type previously
  without `Debug` is now printable.
  Closes [issue #4 — finding 2]https://github.com/findit-studio/mediadecode/issues/4.
- **`#[must_use]`** on every consuming `with_*` builder method
  across the crate's public surface.
  Closes [issue #4 — finding 3]https://github.com/findit-studio/mediadecode/issues/4.

[0.1.0]: https://github.com/findit-studio/mediadecode/releases/tag/mediadecode-ffmpeg-v0.1.0
[0.2.0]: https://github.com/findit-studio/mediadecode/releases/tag/mediadecode-ffmpeg-v0.2.0

## [0.1.0] - 2026-05-09

Initial public release.

### Added

- **Adapter type.** `Ffmpeg` zero-sized type implementing
  `mediadecode::adapter::VideoAdapter`, `AudioAdapter`, and
  `SubtitleAdapter`.
- **Buffer.** `FfmpegBuffer` — zero-copy refcounted view over an
  `AVBufferRef`, with `empty` / `from_packet` / `from_plane`
  constructors and panic-free `try_*` counterparts.
- **Video decoder.** `FfmpegVideoStreamDecoder` mirrors
  `ffmpeg::decoder::Video`'s `send_packet` / `receive_frame` shape and
  auto-probes the host's HW backends — VideoToolbox on Apple,
  VAAPI / CUDA on Linux, D3D11VA / CUDA on Windows — falling through
  to a software decoder when none open. `open_with(_, _, Backend::…)`
  pins a specific backend (no probe).
- **Audio decoder.** `FfmpegAudioStreamDecoder` over
  `ffmpeg::decoder::Audio`, producing zero-copy `AudioFrame`s.
- **Subtitle decoder.** `FfmpegSubtitleStreamDecoder` over the legacy
  synchronous `ffmpeg::decoder::Subtitle::decode` API, bridged into the
  trait's `send_packet` / `receive_frame` shape.
- **Type aliases.** `VideoPacket`, `AudioPacket`, `SubtitlePacket`,
  `VideoFrame`, `AudioFrame`, `SubtitleFrame` — pre-parameterized with
  this crate's adapter, buffer, and extras types.
- **Boundary helpers.** `video_packet_from_ffmpeg`,
  `audio_packet_from_ffmpeg`, `subtitle_packet_from_ffmpeg` — convert a
  borrowed `ffmpeg::Packet` into the matching `mediadecode` packet
  without copying the compressed payload. Empty-frame builders
  `empty_video_frame`, `empty_audio_frame`, `empty_subtitle_frame`
  produce well-formed destinations for `receive_frame`.
- **Recovery.** `VideoDecodeError::AllBackendsFailed { unconsumed_packets, .. }`
  carries any packets the decoder had already accepted from the
  demuxer when every backend is exhausted, so non-seekable callers
  (live streams, pipes, network sources) can replay them through their
  own software decoder without re-demuxing.

### Safety

The FFmpeg FFI surface is hardened against malformed or
version-skewed decoder output:

- All bindgen enum reads go through `addr_of!` + `read_unaligned` to
  avoid creating invalid Rust enum values from raw memory.
- `AVFrameSideDataType` values are mapped through an explicit
  whitelist of known `AV_FRAME_DATA_*` constants — never `transmute`d.
- `CStr::from_ptr` calls are replaced with a bounded
  `bounded_cstr_bytes` helper that searches at most
  `SUBTITLE_MAX_TEXT_BYTES_PER_RECT + 1` bytes for a NUL terminator.
- Signed counts (`AVFrame.nb_side_data`, `AVSubtitle.num_rects`, …)
  are clamped to non-negative values before any `as usize` cast,
  preventing OOB walks under corrupt input.
- Side-data and subtitle conversions enforce caps on entries and total
  bytes (`SIDE_DATA_MAX_ENTRIES`, `SIDE_DATA_MAX_TOTAL_BYTES`,
  `HW_COPY_SIDE_DATA_MAX_*`, `SUBTITLE_MAX_*`).
- `send_packet` consumes the demuxer packet only after the probe
  rescue records it, so a non-seekable caller can rebuild the input
  stream from `unconsumed_packets` on `AllBackendsFailed`.
- `cpu_frame_bytes` sizes against the underlying `AVBufferRef.size`
  rather than `linesize × plane_height_for(AVFrame.height)`, so
  cropped or heavily aligned streams report correct byte counts.