ez-ffmpeg 0.17.2

A safe and ergonomic Rust interface for FFmpeg integration, designed for ease of use.
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
use ffmpeg_next::ffi::AVERROR;
use ffmpeg_sys_next::*;
use std::ffi::NulError;
use std::{io, result};

// The `opengl` module path is deprecated as a whole (superseded by
// `wgpu_filter`), but the crate error enum must still name its typed error;
// importing it here, with the module-path deprecation silenced, keeps the
// variant and thiserror's generated `From` impl warning-free.
#[cfg(feature = "opengl")]
#[allow(deprecated)]
use crate::opengl::OpenGLFilterError;

/// Result type of all ez-ffmpeg library calls.
pub type Result<T, E = Error> = result::Result<T, E>;

/// Top-level error type for all ez-ffmpeg operations.
///
/// Most variants wrap a stage-specific error enum (opening inputs and
/// outputs, demuxing, decoding, filtering, encoding, muxing, ...), so
/// callers can match on the pipeline stage first and inspect the typed
/// cause when they need to.
#[derive(thiserror::Error, Debug)]
#[non_exhaustive]
pub enum Error {
    /// Returned when an operation requires a scheduler that has been
    /// started, but it has not been.
    #[error("Scheduler is not started")]
    NotStarted,

    /// A URL or path string could not be converted into a C string for
    /// FFmpeg (see [`UrlError`]).
    #[error("URL error: {0}")]
    Url(#[from] UrlError),

    /// Opening an input file, stream, device, or custom input source failed.
    #[error("Open input stream error: {0}")]
    OpenInputStream(#[from] OpenInputError),

    /// Probing stream information from an opened input failed.
    #[error("Find stream info error: {0}")]
    FindStream(#[from] FindStreamError),

    /// Resolving a decoder failed (see [`DecoderError`]).
    #[error("Decoder error: {0}")]
    Decoder(#[from] DecoderError),

    /// Parsing a filtergraph description failed.
    #[error("Filter graph parse error: {0}")]
    FilterGraphParse(#[from] FilterGraphParseError),

    /// Returned when a filtergraph link label could not be converted to a
    /// UTF-8 string.
    #[error("Filter description converted to utf8 string error")]
    FilterDescUtf8,

    /// Returned when a filter name could not be converted to a UTF-8 string.
    #[error("Filter name converted to utf8 string error")]
    FilterNameUtf8,

    /// Returned when a filtergraph declares zero outputs, which is not
    /// supported.
    #[error("A filtergraph has zero outputs, this is not supported")]
    FilterZeroOutputs,

    /// Returned when a filtergraph declares zero inputs, which is not
    /// supported.
    #[error("A filtergraph has zero inputs, this is not supported")]
    FilterZeroInputs,

    /// Returned when a numeric field — such as a file index in a stream
    /// specifier or link label — could not be parsed as an integer.
    #[error("Input is not a valid number")]
    ParseInteger,

    /// Allocating an output format context failed.
    #[error("Alloc output context error: {0}")]
    AllocOutputContext(#[from] AllocOutputContextError),

    /// Opening or configuring an output failed.
    #[error("Open output error: {0}")]
    OpenOutput(#[from] OpenOutputError),

    /// Returned when an output URL is identical to one of the input URLs;
    /// the payload is the offending path. In-place editing is not supported.
    #[error("Output file '{0}' is the same as an input file")]
    FileSameAsInput(String),

    /// Enumerating capture devices failed.
    #[error("Find devices error: {0}")]
    FindDevices(#[from] FindDevicesError),

    /// Allocating an `AVFrame` failed.
    #[error("Alloc frame error: {0}")]
    AllocFrame(#[from] AllocFrameError),

    /// Allocating an `AVPacket` failed.
    #[error("Alloc packet error: {0}")]
    AllocPacket(#[from] AllocPacketError),

    /// Making a frame's data buffers writable failed (see
    /// [`FrameWritableError`]).
    #[error("Frame writable error: {0}")]
    FrameWritable(#[from] FrameWritableError),

    // ---- Muxing ----
    /// A muxing operation failed while writing the output container.
    #[error("Muxing operation failed {0}")]
    Muxing(#[from] MuxingOperationError),

    // ---- Open Encoder ----
    /// Opening or configuring an encoder failed.
    #[error("Open encoder operation failed {0}")]
    OpenEncoder(#[from] OpenEncoderOperationError),

    // ---- Encoding ----
    /// An encoding operation failed.
    #[error("Encoding operation failed {0}")]
    Encoding(#[from] EncodingOperationError),

    // ---- FilterGraph ----
    /// A filtergraph runtime operation failed.
    #[error("Filter graph operation failed {0}")]
    FilterGraph(#[from] FilterGraphOperationError),

    // ---- Open Decoder ----
    /// Opening or configuring a decoder failed.
    #[error("Open decoder operation failed {0}")]
    OpenDecoder(#[from] OpenDecoderOperationError),

    // ---- Decoding ----
    /// A decoding operation failed.
    #[error("Decoding operation failed {0}")]
    Decoding(#[from] DecodingOperationError),

    // ---- Demuxing ----
    /// A demuxing operation failed.
    #[error("Demuxing operation failed {0}")]
    Demuxing(#[from] DemuxingOperationError),

    // ---- Packet Scanner ----
    /// A packet-scanning operation failed (see [`PacketScannerError`]).
    #[error("Packet scanner error: {0}")]
    PacketScanner(#[from] PacketScannerError),

    // ---- Frame Filter ----
    /// A frame filter failed to initialize; carries the error returned by
    /// the filter's `init`.
    #[error("Frame filter init failed: {0}")]
    FrameFilterInit(Box<dyn std::error::Error + Send + Sync>),

    /// A frame filter failed while processing a frame; carries the error
    /// returned by the filter's `filter_frame`.
    #[error("Frame filter process failed: {0}")]
    FrameFilterProcess(Box<dyn std::error::Error + Send + Sync>),

    /// A frame filter failed while generating a frame; carries the error
    /// returned by the filter's `request_frame`.
    #[error("Frame filter request failed: {0}")]
    FrameFilterRequest(Box<dyn std::error::Error + Send + Sync>),

    /// Returned while building a frame pipeline when no stream of the
    /// required media type exists at the named pipeline end; fields are the
    /// pipeline end (input/output) and the media type.
    #[error("No {0} stream of the type:{1} were found while build frame pipeline")]
    FrameFilterTypeNoMatched(String, String),

    /// Returned while building a frame pipeline when no stream at the named
    /// pipeline end matches both the requested stream index and media type;
    /// fields are the pipeline end, the stream index, and the media type.
    #[error("{0} stream:{1} of the type:{2} were mismatched while build frame pipeline")]
    FrameFilterStreamTypeNoMatched(String, usize, String),

    /// Returned when a frame pipeline tries to deliver a frame to a
    /// destination that has already finished.
    #[error("Frame filter pipeline destination already finished")]
    FrameFilterDstFinished,

    /// Returned when a frame pipeline fails to duplicate a frame required
    /// for an additional destination.
    #[error("Frame filter pipeline failed to duplicate a frame for an additional destination")]
    FrameFilterFrameDuplicateFailed,

    /// Returned when spawning a frame pipeline's worker thread fails, so
    /// the pipeline never ran.
    #[error("Frame filter pipeline thread exited")]
    FrameFilterThreadExited,

    /// A worker thread panicked; the payload is the worker's thread name.
    /// Output may be incomplete.
    #[error("Worker thread '{0}' panicked; output may be incomplete")]
    WorkerPanicked(String),

    /// Recorded as the scheduler result when `start()` fails after some
    /// worker threads were already launched. `start()` itself returns the
    /// actual init error to its caller; this recorded value is what
    /// concurrent observers (packet-sink terminal callbacks) report, so a
    /// sink can never mistake a torn-down startup for a settled-Ok job.
    #[error("Scheduler start failed; the job was torn down during startup")]
    StartFailed,

    /// Returned when publishing to the embedded RTMP server with a stream
    /// key that is already in use; the payload is the key.
    #[cfg(feature = "rtmp")]
    #[error("Rtmp stream already exists with key: {0}")]
    RtmpStreamAlreadyExists(String),

    /// Returned when a stream could not be created on the embedded RTMP
    /// server, typically because the server has stopped.
    #[cfg(feature = "rtmp")]
    #[error("Rtmp create stream failed. Check whether the server is stopped.")]
    RtmpCreateStream,

    /// Returned when too many streams are waiting to be registered on the
    /// embedded RTMP server.
    #[cfg(feature = "rtmp")]
    #[error("Rtmp registration queue is full: too many streams are waiting to be registered")]
    RtmpRegistrationQueueFull,

    /// Returned when the embedded RTMP server's thread has exited.
    #[cfg(feature = "rtmp")]
    #[error("Rtmp server thread exited")]
    RtmpThreadExited,

    /// Returned when the embedded RTMP server is no longer consuming a
    /// published stream.
    #[cfg(feature = "rtmp")]
    #[error("Rtmp stream closed: the server is no longer consuming this stream")]
    RtmpStreamClosed,

    /// Returned when starting an embedded RTMP server that was already
    /// started; clones of one server share a single lifecycle that can be
    /// started only once.
    #[cfg(feature = "rtmp")]
    #[error("Rtmp server already started: clones of one server share a single lifecycle, which can be started only once")]
    RtmpServerAlreadyStarted,

    /// A subtitle processing operation failed.
    #[cfg(feature = "subtitle")]
    #[error("Subtitle error: {0}")]
    Subtitle(#[from] crate::subtitle::SubtitleError),

    /// A wgpu GPU filter operation failed.
    #[cfg(feature = "wgpu")]
    #[error("Wgpu filter error: {0}")]
    WgpuFilter(#[from] crate::wgpu_filter::WgpuFilterError),

    // The allow covers the deprecation that OpenGLFilterError inherits from
    // the deprecated `opengl` module; the variant must still carry the type.
    // From is hand-written below the enum (a derived #[from] would re-name
    // the type in generated code that no #[allow] on the variant reaches).
    /// An OpenGL filter operation failed (deprecated `opengl` feature).
    #[cfg(feature = "opengl")]
    #[allow(deprecated)]
    #[error("OpenGL filter error: {0}")]
    OpenGLFilter(#[source] OpenGLFilterError),

    /// An I/O error from the standard library.
    #[error("IO error:{0}")]
    IO(#[from] io::Error),

    /// Internal end-of-stream marker passed between pipeline stages; a
    /// normal end of input is consumed internally rather than reported as
    /// a job failure.
    #[error("EOF")]
    EOF,
    /// Internal control-flow marker instructing pipeline stages to shut
    /// down; normally consumed internally.
    #[error("Exit")]
    Exit,
    /// Internal invariant violation that should never occur; indicates a
    /// bug in this crate rather than a problem with user input.
    #[error("Bug")]
    Bug,

    /// Returned when a recipe or analysis option is invalid (out of range,
    /// malformed, or inconsistent); the payload describes the problem.
    #[error("Invalid recipe argument: {0}")]
    InvalidRecipeArg(String),

    /// A container-info query was called with an out-of-range index.
    #[error("Container info error: {0}")]
    ContainerInfo(#[from] ContainerInfoError),

    /// A frame-export operation failed.
    #[error("Frame export error: {0}")]
    FrameExport(#[from] crate::core::frame_export::FrameExportError),

    /// Building or opening a video writer failed.
    #[error("Video writer error: {0}")]
    Writer(#[from] crate::core::writer::WriterError),

    /// Pushing a frame into a video writer failed.
    #[error("Video writer push error: {0}")]
    Push(#[from] crate::core::writer::PushError),

    /// Returned when a frame-source input's worker thread failed to start.
    #[error("Frame source thread failed to start")]
    FrameSourceThreadExited,

    /// A packet-sink output failed (see [`PacketSinkError`]).
    #[error("Packet sink error: {0}")]
    PacketSink(#[from] PacketSinkError),

    /// CLI-compat pipelines only: a `-vf` command was lowered onto an input
    /// whose OPENED demuxer does not carry exactly one video stream. The
    /// check runs on the demuxer instance the pipeline actually executes
    /// with (no separate probe opening, no TOCTOU window). The facade maps
    /// this to its public `AmbiguousFilterSource` diagnostic.
    #[cfg(feature = "cli")]
    #[error("the per-output video filter requires exactly one video stream in the input; the opened input has {video_streams}")]
    AmbiguousVideoSource {
        /// Number of video streams carried by the opened input.
        video_streams: usize,
    },

    /// Strict AVOption handling (CLI-compat pipelines): an option the caller
    /// supplied was not consumed by the component it targeted. The default
    /// builder path only WARNS about such leftovers; pipelines built through
    /// the `cli` feature's entry points fail instead, mirroring fftools'
    /// `check_avoptions` abort. Only exists with the `cli` feature — the
    /// feature-off API surface is unchanged.
    #[cfg(feature = "cli")]
    #[error("option '{option}' was not consumed by {site}; CLI-compat strict mode treats leftover AVOptions as errors")]
    UnconsumedCliOption {
        /// Human-readable description of the component that should have
        /// consumed the option (e.g. "the muxer of output 0").
        site: String,
        /// The option key that was left unconsumed.
        option: String,
    },
}

// `Error` rides in every hot-path `Result` — the per-frame encoder and filter
// calls return `Result<(), Error>` / `Result<bool, Error>` — so its size is a
// layout contract, not an implementation detail: one oversized payload grows
// every such `Result` crate-wide. 64 bytes is the long-standing layout; keep
// new payloads inside it (use static labels for fixed vocabulary, or box a
// genuinely large variant). A const assertion rather than a #[test] so that
// merely compiling the crate enforces the bound for whichever feature-gated
// variants that build carries — including feature combinations whose tests
// are compiled but never run.
const _: () = assert!(
    std::mem::size_of::<Error>() <= 64,
    "Error grew past its 64-byte layout: shrink the new payload (static labels) or box the variant"
);

/// Builder/open-time validation errors for [`crate::VideoWriter`]. Exported here
/// (not from the crate root) to mirror the existing `OpenInputError` /
/// `OpenOutputError` organization; the root surface stays the settled writer
/// types (the writer itself, its builder, and the push error pair).
pub use crate::core::writer::WriterError;

// Hand-written counterpart of the #[from] the sibling variants derive: the
// error type inherits deprecation from the deprecated `opengl` module, so
// the conversion is spelled out where the lint can be silenced.
#[cfg(feature = "opengl")]
#[allow(deprecated)]
impl From<OpenGLFilterError> for Error {
    fn from(err: OpenGLFilterError) -> Self {
        Error::OpenGLFilter(err)
    }
}

/// Errors from the `container_info` queries where the caller asked for an index
/// outside the container's range. These are caller/argument errors — a bad index
/// into an otherwise valid container — kept distinct from an open/probe failure
/// (`OpenInputError` / `FindStreamError`) so retry logic, telemetry, and user
/// messages can tell "you asked for chapter 5 of a 3-chapter file" apart from
/// "the file is corrupt or unreadable". Each variant carries the offending
/// `index` and the container's actual `count`.
#[derive(thiserror::Error, Debug)]
#[non_exhaustive]
pub enum ContainerInfoError {
    /// Returned when the requested chapter index exceeds the number of
    /// chapters in the container.
    #[error("chapter index {index} out of range: the container has {count} chapter(s)")]
    ChapterIndexOutOfRange {
        /// The chapter index that was requested.
        index: usize,
        /// Number of chapters the container actually has.
        count: usize,
    },

    /// Returned when the requested stream index exceeds the number of
    /// streams in the container.
    #[error("stream index {index} out of range: the container has {count} stream(s)")]
    StreamIndexOutOfRange {
        /// The stream index that was requested.
        index: usize,
        /// Number of streams the container actually has.
        count: usize,
    },
}

/// Error type for RTMP streaming operations using StreamBuilder
#[cfg(feature = "rtmp")]
#[derive(thiserror::Error, Debug)]
#[non_exhaustive]
pub enum StreamError {
    /// Returned when a required builder parameter was never set; the
    /// payload is the parameter name (e.g. "address", "stream_key").
    #[error("missing required parameter: {0}")]
    MissingParameter(&'static str),

    /// Returned when the configured input path does not point to an
    /// existing file.
    #[error("input path is not a valid file: {path}")]
    InputNotFound {
        /// The input path that failed validation.
        path: std::path::PathBuf,
    },

    /// An underlying ez-ffmpeg error raised while building or running the
    /// stream.
    #[error("ffmpeg error: {0}")]
    Ffmpeg(#[from] crate::error::Error),
}

impl PartialEq for Error {
    /// Structural equality for payload-less variants only. Variants carrying
    /// an inner error compare unequal even to themselves — use matches! on
    /// the variant when that is what you mean.
    fn eq(&self, other: &Self) -> bool {
        use Error::*;
        match (self, other) {
            (NotStarted, NotStarted)
            | (FilterDescUtf8, FilterDescUtf8)
            | (FilterNameUtf8, FilterNameUtf8)
            | (FilterZeroOutputs, FilterZeroOutputs)
            | (FilterZeroInputs, FilterZeroInputs)
            | (ParseInteger, ParseInteger)
            | (FrameFilterDstFinished, FrameFilterDstFinished)
            | (FrameFilterFrameDuplicateFailed, FrameFilterFrameDuplicateFailed)
            | (FrameFilterThreadExited, FrameFilterThreadExited)
            | (FrameSourceThreadExited, FrameSourceThreadExited)
            | (EOF, EOF)
            | (Exit, Exit)
            | (Bug, Bug) => true,
            #[cfg(feature = "rtmp")]
            (RtmpCreateStream, RtmpCreateStream)
            | (RtmpRegistrationQueueFull, RtmpRegistrationQueueFull)
            | (RtmpThreadExited, RtmpThreadExited)
            | (RtmpStreamClosed, RtmpStreamClosed)
            | (RtmpServerAlreadyStarted, RtmpServerAlreadyStarted) => true,
            _ => false,
        }
    }
}

// No Eq impl: variants carrying payloads are not equal to themselves, so
// the relation is not reflexive and claiming Eq would be a lie.

/// Errors from the demuxer stage while reading packets from an input.
/// Variants carrying a [`DemuxingError`] embed the mapped FFmpeg error.
#[derive(thiserror::Error, Debug)]
#[non_exhaustive]
pub enum DemuxingOperationError {
    /// Returned when reading the next packet from the input fails
    /// (`av_read_frame`).
    #[error("while reading frame: {0}")]
    ReadFrameError(DemuxingError),

    /// Returned when creating an additional reference to a demuxed packet
    /// fails (`av_packet_ref`).
    #[error("while referencing packet: {0}")]
    PacketRefError(DemuxingError),

    /// Returned when seeking in the input fails (`avformat_seek_file`).
    #[error("while seeking file: {0}")]
    SeekFileError(DemuxingError),

    /// Returned when spawning the demuxer thread fails, so demuxing never
    /// started.
    #[error("Thread exited")]
    ThreadExited,
}

/// Errors from the decoder stage while turning packets into frames.
/// Variants carrying a [`DecodingError`] embed the mapped FFmpeg error.
#[derive(thiserror::Error, Debug)]
#[non_exhaustive]
pub enum DecodingOperationError {
    /// Returned when creating a new reference to a decoded frame fails
    /// (`av_frame_ref`).
    #[error("during frame reference creation: {0}")]
    FrameRefError(DecodingError),

    /// Returned when copying frame metadata fails (`av_frame_copy_props`).
    #[error("during frame properties copy: {0}")]
    FrameCopyPropsError(DecodingError),

    /// Returned when decoding a subtitle packet fails
    /// (`avcodec_decode_subtitle2`).
    #[error("during subtitle decoding: {0}")]
    DecodeSubtitleError(DecodingError),

    /// Returned when copying a decoded subtitle for delivery fails.
    #[error("during subtitle copy: {0}")]
    CopySubtitleError(DecodingError),

    /// Returned when submitting a packet to the decoder fails
    /// (`avcodec_send_packet`).
    #[error("during packet submission to decoder: {0}")]
    SendPacketError(DecodingError),

    /// Returned when receiving a decoded frame from the decoder fails
    /// (`avcodec_receive_frame`).
    #[error("during frame reception from decoder: {0}")]
    ReceiveFrameError(DecodingError),

    /// Returned when allocating a frame during decoding fails.
    #[error("during frame allocation: {0}")]
    FrameAllocationError(DecodingError),

    /// Returned when allocating a packet during decoding fails.
    #[error("during packet allocation: {0}")]
    PacketAllocationError(DecodingError),

    /// Returned when allocating an `AVSubtitle` during decoding fails.
    #[error("during AVSubtitle allocation: {0}")]
    SubtitleAllocationError(DecodingError),

    /// Returned when the decoder emits a frame flagged as corrupt and
    /// corrupt frames are treated as errors.
    #[error("corrupt decoded frame")]
    CorruptFrame,

    /// Returned when the ratio of decode errors to decoded frames exceeds
    /// the maximum allowed rate.
    #[error("decode error rate exceeded the maximum allowed")]
    ErrorRateExceeded,

    /// Returned when downloading a hardware-decoded frame to system memory
    /// fails (`av_hwframe_transfer_data`).
    #[error("during retrieve data on hw: {0}")]
    HWRetrieveDataError(DecodingError),

    /// Returned when applying codec cropping metadata to a decoded frame
    /// fails.
    #[error("during cropping: {0}")]
    CroppingError(DecodingError),
}

/// Errors from opening and configuring a decoder.
/// Variants carrying an [`OpenDecoderError`] embed the mapped FFmpeg error.
#[derive(thiserror::Error, Debug)]
#[non_exhaustive]
pub enum OpenDecoderOperationError {
    /// Returned when allocating the decoder context fails
    /// (`avcodec_alloc_context3`).
    #[error("during context allocation: {0}")]
    ContextAllocationError(OpenDecoderError),

    /// Returned when applying the stream's codec parameters to the decoder
    /// context fails (`avcodec_parameters_to_context`).
    #[error("while applying parameters to context: {0}")]
    ParameterApplicationError(OpenDecoderError),

    /// Returned when opening the decoder fails (`avcodec_open2`).
    #[error("while opening decoder: {0}")]
    DecoderOpenError(OpenDecoderError),

    /// Returned when copying the audio channel layout into the decoder
    /// context fails.
    #[error("while copying channel layout: {0}")]
    ChannelLayoutCopyError(OpenDecoderError),

    /// Returned when setting up hardware acceleration for the decoder
    /// fails.
    #[error("while Hw setup: {0}")]
    HwSetupError(OpenDecoderError),

    /// Returned when the configured decoder name is invalid.
    #[error("Invalid decoder name")]
    InvalidName,

    /// Returned when spawning the decoder thread fails, so the decoder
    /// never opened.
    #[error("Thread exited")]
    ThreadExited,
}

/// Errors from running frames through a configured filtergraph.
/// Variants carrying a [`FilterGraphError`] embed the mapped FFmpeg error.
#[derive(thiserror::Error, Debug)]
#[non_exhaustive]
pub enum FilterGraphOperationError {
    /// Returned when requesting the next frame from the graph fails
    /// (`avfilter_graph_request_oldest`).
    #[error("during requesting oldest frame: {0}")]
    RequestOldestError(FilterGraphError),

    /// Returned when processing frames through the filtergraph fails.
    #[error("during process frames: {0}")]
    ProcessFramesError(FilterGraphError),

    /// Returned when sending frames into the filtergraph fails.
    #[error("during send frames: {0}")]
    SendFramesError(FilterGraphError),

    /// Returned when copying an audio channel layout while configuring the
    /// graph fails.
    #[error("during copying channel layout: {0}")]
    ChannelLayoutCopyError(FilterGraphError),

    /// Returned when pushing a frame into a graph input fails
    /// (`av_buffersrc_add_frame`).
    #[error("during buffer source add frame: {0}")]
    BufferSourceAddFrameError(FilterGraphError),

    /// Returned when closing a graph input at end of stream fails
    /// (`av_buffersrc_close`).
    #[error("during closing buffer source: {0}")]
    BufferSourceCloseError(FilterGraphError),

    /// Returned when replacing a frame's buffer reference fails
    /// (`av_buffer_replace`).
    #[error("during replace buffer: {0}")]
    BufferReplaceoseError(FilterGraphError),

    /// Returned when cloning frame side data for the graph fails.
    #[error("during cloning frame side data: {0}")]
    FrameSideDataCloneError(FilterGraphError),

    /// Returned when parsing or configuring the filtergraph description
    /// fails.
    #[error("during parse: {0}")]
    ParseError(FilterGraphParseError),

    /// Returned when a frame entering the graph carries invalid or
    /// corrupted data.
    #[error("The data in the frame is invalid or corrupted")]
    InvalidData,

    /// Returned before the graph is configured when one input has buffered
    /// frames past the admission limit while another input has not yet
    /// delivered its first frame; fields are the input label, the buffered
    /// frame count, and the estimated retained memory in bytes.
    #[error(
        "graph input '{0}' already holds {1} buffered frames and admitting the next \
         one would raise the best-effort retained-memory estimate to ~{2} bytes, \
         while another input has not yet delivered its first frame, so the filter \
         graph cannot be configured; check that every graph input actually produces \
         data (or produces it within the buffering window)"
    )]
    PreConfigQueueOverflow(String, usize, usize),

    // Only constructed on the FFmpeg 8+ buffersrc side-data clone path.
    /// Returned when a frame's combined side-data metadata is too large to
    /// deep-copy into the buffersrc parameters; fields are the input label
    /// and the estimated size in bytes.
    #[cfg_attr(not(ffmpeg_8_0), allow(dead_code))]
    #[error(
        "graph input '{0}' would deep-copy an estimated {1} bytes of side-data \
         metadata into the buffersrc parameters, exceeding the side-data clone \
         estimate threshold; the frame's combined side-data metadata (across its \
         global and downmix entries) is pathologically large"
    )]
    OversizedSideDataClone(String, usize),

    /// Returned when spawning the filtergraph thread fails, so the graph
    /// never ran.
    #[error("Thread exited")]
    ThreadExited,
}

/// Errors from the encoder stage while turning frames into packets.
/// Variants carrying an [`EncodingError`] embed the mapped FFmpeg error.
#[derive(thiserror::Error, Debug)]
#[non_exhaustive]
pub enum EncodingOperationError {
    /// Returned when submitting a frame to the encoder fails
    /// (`avcodec_send_frame`).
    #[error("during frame submission: {0}")]
    SendFrameError(EncodingError),

    /// Returned when receiving an encoded packet from the encoder fails
    /// (`avcodec_receive_packet`).
    #[error("during packet retrieval: {0}")]
    ReceivePacketError(EncodingError),

    /// Returned when re-chunking buffered audio samples into encoder-sized
    /// frames fails.
    #[error("during audio frame receive: {0}")]
    ReceiveAudioError(EncodingError),

    /// Returned when a subtitle packet reaches the encoder without a
    /// presentation timestamp.
    #[error(": Subtitle packets must have a pts")]
    SubtitleNotPts,

    /// Returned when an encoded packet cannot be delivered because the
    /// muxer has already finished.
    #[error(": Muxer already finished")]
    MuxerFinished,

    /// An output stream buffered more packets before the muxer started than the
    /// pre-mux queue admits (fftools `AVERROR_BUFFER_TOO_SMALL`, "Too many
    /// packets buffered for output stream"). Unlike `MuxerFinished` this is a
    /// hard failure — never a silent truncation — so it must reach the
    /// scheduler error, not the graceful stop path.
    #[error(": too many packets buffered for an output stream before the muxer started; raise Output::set_max_muxing_queue_size / Output::set_muxing_queue_data_threshold, or check that every mapped output stream receives data")]
    MuxQueueFull,

    /// Returned when encoding a subtitle fails (see
    /// [`EncodeSubtitleError`]).
    #[error("Encode subtitle error: {0}")]
    EncodeSubtitle(#[from] EncodeSubtitleError),

    /// Returned when allocating a packet for encoder output fails.
    #[error(": {0}")]
    AllocPacket(AllocPacketError),
}

/// Errors from the muxer stage while writing the output container.
/// Variants carrying a [`MuxingError`] embed the mapped FFmpeg error.
#[derive(thiserror::Error, Debug)]
#[non_exhaustive]
pub enum MuxingOperationError {
    /// Returned when writing the container header fails (see
    /// [`WriteHeaderError`]).
    #[error("during write header: {0}")]
    WriteHeader(WriteHeaderError),

    /// Returned when initializing a bitstream filter chain for an output
    /// stream fails; fields are the chain description and the underlying
    /// error.
    #[error("while initializing bitstream filter chain '{0}': {1}")]
    BitstreamFilterInit(String, MuxingError),

    /// Returned when writing an interleaved packet to the container fails
    /// (`av_interleaved_write_frame`).
    #[error("during interleaved write: {0}")]
    InterleavedWriteError(MuxingError),

    /// Returned when writing the container trailer fails
    /// (`av_write_trailer`).
    #[error("during trailer write: {0}")]
    TrailerWriteError(MuxingError),

    /// Returned when closing the output I/O context fails.
    #[error("during closing IO: {0}")]
    IOCloseError(MuxingError),

    /// Returned when spawning the muxer (or mux-init) thread fails, so
    /// muxing never started.
    #[error("Thread exited")]
    ThreadExited,
}

/// Errors specific to packet-sink outputs (`Output::new_by_packet_sink`).
///
/// The strict tier fails fast: configuration problems surface from `build()`
/// or from the job **before any sink callback runs**; per-packet violations
/// stop the job with the offending packet never delivered. `Clone` is
/// deliberate — for delivery-path errors the same value is recorded as the
/// job error and handed to the sink's `on_delivery_error` callback.
/// [`JobFailed`](Self::JobFailed) is the exception: it is synthesized for
/// that callback only, while first-error-wins may leave the job result owned
/// by a sibling worker's error.
#[derive(thiserror::Error, Debug, Clone)]
#[non_exhaustive]
pub enum PacketSinkError {
    /// A builder option the packet sink cannot honor was set: either a
    /// container-only option (no container is written, so it could never
    /// take effect) or a pipeline feature outside the strict tier's
    /// delivery contract (filters, bitstream filters, subtitle codecs —
    /// rejected as policy, not for lack of a container).
    #[error("{0} is not supported on packet-sink outputs")]
    UnsupportedOption(&'static str),

    /// A stream was configured as `copy`; packet sinks require encoded
    /// streams.
    #[error("stream copy is not supported on packet-sink outputs (strict tier requires encoded streams)")]
    StreamCopyUnsupported,

    /// The output mapped a stream the strict tier cannot deliver (non-H.264
    /// video, non-AAC audio, or a non-audio/video kind).
    #[error("{kind} streams are not supported on packet-sink outputs (strict tier)")]
    UnsupportedStream {
        /// Label describing the rejected stream kind (e.g. "non-H.264
        /// video", "non-AAC audio").
        kind: &'static str,
    },

    /// The configured encoder is outside the strict-tier v1 whitelist.
    #[error("encoder '{encoder}' is not on the strict-tier whitelist for {kind} (v1 accepts: {allowed})")]
    EncoderNotWhitelisted {
        /// Media kind of the stream ("video" or "audio").
        kind: &'static str,
        /// The encoder name that was configured.
        encoder: String,
        /// The encoder names the strict tier accepts for this kind.
        allowed: &'static str,
    },

    /// No stream was mapped to the packet-sink output.
    #[error("packet-sink output has no streams")]
    NoStreams,

    /// An encoder finalized without the out-of-band codec configuration the
    /// strict tier delivers via `on_stream_info`.
    #[error("output stream {stream_index}: encoder produced no extradata; the strict tier requires codec configuration (avcC / AudioSpecificConfig) before the first callback")]
    MissingExtradata {
        /// Index of the offending output stream.
        stream_index: usize,
    },

    /// The encoder's codec configuration failed strict-tier validation.
    #[error("output stream {stream_index}: invalid codec configuration: {reason}")]
    InvalidExtradata {
        /// Index of the offending output stream.
        stream_index: usize,
        /// Why the codec configuration failed validation.
        reason: String,
    },

    /// A stream's time base is not a positive rational.
    #[error("output stream {stream_index}: invalid time base {num}/{den} (positive numerator and denominator required)")]
    InvalidTimeBase {
        /// Index of the offending output stream.
        stream_index: usize,
        /// Numerator of the rejected time base.
        num: i32,
        /// Denominator of the rejected time base.
        den: i32,
    },

    /// A packet was stamped in a time base other than its stream's.
    #[error("output stream {stream_index}: packet time base {packet_num}/{packet_den} differs from the stream time base {stream_num}/{stream_den}")]
    PacketTimeBaseMismatch {
        /// Index of the offending output stream.
        stream_index: usize,
        /// Numerator of the packet's time base.
        packet_num: i32,
        /// Denominator of the packet's time base.
        packet_den: i32,
        /// Numerator of the stream's time base.
        stream_num: i32,
        /// Denominator of the stream's time base.
        stream_den: i32,
    },

    /// A packet carries no pts or dts (`AV_NOPTS_VALUE`).
    #[error("output stream {stream_index}: packet carries no {which} (strict tier rejects AV_NOPTS_VALUE)")]
    MissingTimestamp {
        /// Index of the offending output stream.
        stream_index: usize,
        /// Which timestamp is missing: "pts" or "dts".
        which: &'static str,
    },

    /// A packet's dts did not strictly increase within its stream.
    #[error("output stream {stream_index}: non-monotonic dts (previous {prev}, current {current})")]
    NonMonotonicDts {
        /// Index of the offending output stream.
        stream_index: usize,
        /// dts of the previous packet, in stream time-base units.
        prev: i64,
        /// dts of the offending packet, in stream time-base units.
        current: i64,
    },

    /// A packet's pts collided with a still-pending pts on the same stream.
    #[error("output stream {stream_index}: duplicate pts {pts}")]
    DuplicatePts {
        /// Index of the offending output stream.
        stream_index: usize,
        /// The duplicated pts value, in stream time-base units.
        pts: i64,
    },

    /// A packet's pts is earlier than its dts.
    #[error("output stream {stream_index}: pts {pts} is earlier than dts {dts}")]
    PtsBeforeDts {
        /// Index of the offending output stream.
        stream_index: usize,
        /// The packet's pts, in stream time-base units.
        pts: i64,
        /// The packet's dts, in stream time-base units.
        dts: i64,
    },

    /// Rescaling a timestamp onto the shared time origin overflowed.
    #[error("output stream {stream_index}: timestamp overflow while applying the shared time origin")]
    TimestampOverflow {
        /// Index of the offending output stream.
        stream_index: usize,
    },

    /// A packet has no positive duration and none could be derived from the
    /// stream configuration (frame rate / codec frame size).
    #[error("output stream {stream_index}: packet duration is absent and cannot be derived (strict tier requires a positive duration)")]
    MissingDuration {
        /// Index of the offending output stream.
        stream_index: usize,
    },

    /// The packet payload failed bitstream validation.
    #[error("output stream {stream_index}: malformed packet payload: {reason}")]
    MalformedPacket {
        /// Index of the offending output stream.
        stream_index: usize,
        /// Description of the bitstream validation failure.
        reason: String,
    },

    /// Internal sequencing violation: a packet surfaced outside the delivery
    /// phase.
    #[error("output stream {stream_index}: packet processed outside the delivery phase (internal sequencing violation)")]
    PhaseViolation {
        /// Index of the offending output stream.
        stream_index: usize,
    },

    /// The stream configuration changed after `on_stream_info` delivered it.
    #[error("output stream {stream_index}: mid-stream configuration change ({what}); the strict tier requires an immutable stream configuration")]
    ConfigChange {
        /// Index of the offending output stream.
        stream_index: usize,
        /// Description of the configuration change that was detected.
        what: String,
    },

    /// An H.264 access unit carried in-band SPS/PPS parameter sets.
    #[error("output stream {stream_index}: in-band SPS/PPS parameter sets are not supported in the strict tier (WebCodecs avc requires out-of-band configuration)")]
    InBandParameterSets {
        /// Index of the offending output stream.
        stream_index: usize,
    },

    /// The sink's `on_stream_info` callback rejected the configuration.
    #[error("on_stream_info callback rejected the stream configuration: {error}")]
    StreamInfoCallbackFailed {
        /// The error the callback returned.
        #[source]
        error: crate::core::packet_sink::PacketCallbackError,
    },

    /// The sink's `on_packet` callback returned an error.
    #[error("on_packet callback failed on output stream {stream_index}: {error}")]
    PacketCallbackFailed {
        /// Index of the offending output stream.
        stream_index: usize,
        /// The error the callback returned.
        #[source]
        error: crate::core::packet_sink::PacketCallbackError,
    },

    /// The channel adapter's receiver was dropped, cancelling delivery and
    /// the job.
    #[error("the packet-sink channel receiver was dropped; delivery cancelled")]
    ChannelDisconnected,

    /// The job failed outside this sink's delivery path; handed to
    /// `on_delivery_error` only, while `wait()` keeps the original error.
    #[error("the job failed outside this packet sink; delivery may have been truncated: {message}")]
    JobFailed {
        /// Display rendering of the error that actually failed the job.
        message: String,
    },
}

/// Errors from opening and configuring an encoder.
/// Variants carrying an [`OpenEncoderError`] embed the mapped FFmpeg error.
#[derive(thiserror::Error, Debug)]
#[non_exhaustive]
pub enum OpenEncoderOperationError {
    /// Returned when cloning frame side data into the encoder context
    /// fails.
    #[error("during frame side data cloning: {0}")]
    FrameSideDataCloneError(OpenEncoderError),

    /// Returned when copying the audio channel layout into the encoder
    /// context fails.
    #[error("during channel layout copying: {0}")]
    ChannelLayoutCopyError(OpenEncoderError),

    /// Returned when opening the encoder fails (`avcodec_open2`).
    #[error("during codec opening: {0}")]
    CodecOpenError(OpenEncoderError),

    /// Returned when exporting encoder parameters to the output stream
    /// fails (`avcodec_parameters_from_context`).
    #[error("while setting codec parameters: {0}")]
    CodecParametersError(OpenEncoderError),

    /// Returned when the format of the frame to encode is unknown.
    #[error(": unknown format of the frame")]
    UnknownFrameFormat,

    /// Returned when configuring subtitle encoding parameters fails.
    #[error("while setting subtitle: {0}")]
    SettingSubtitleError(OpenEncoderError),

    /// Returned when setting up hardware acceleration for the encoder
    /// fails.
    #[error("while Hw setup: {0}")]
    HwSetupError(OpenEncoderError),

    /// Returned when allocating the encoder context fails
    /// (`avcodec_alloc_context3`).
    #[error("during context allocation: {0}")]
    ContextAllocationError(OpenEncoderError),

    /// Returned when the frame stream ends (EOF or upstream disconnect)
    /// before the encoder received any frame, so the encoder was never
    /// opened.
    #[error(": no frames were received before EOF; encoder never opened")]
    NoFramesReceived,

    /// Returned when the stream's media type cannot be encoded (not video,
    /// audio, or subtitle).
    #[error(": unsupported media type for encoding")]
    UnsupportedMediaType,

    /// Returned when spawning the encoder thread fails, so the encoder
    /// never started.
    #[error("Thread exited")]
    ThreadExited,
}

/// Errors from converting URL or path strings for FFmpeg.
#[derive(thiserror::Error, Debug)]
#[non_exhaustive]
pub enum UrlError {
    /// Returned when the string contains an interior NUL byte, which C
    /// strings cannot represent; the payload is the byte position.
    #[error("Null byte found in string at position {0}")]
    NullByteError(usize),
}

impl From<NulError> for Error {
    fn from(err: NulError) -> Self {
        Error::Url(UrlError::NullByteError(err.nul_position()))
    }
}

/// Errors from opening an input file, stream, device, or custom input
/// source. Most variants are mapped from the FFmpeg error code returned by
/// `avformat_open_input`.
#[derive(thiserror::Error, Debug)]
#[non_exhaustive]
pub enum OpenInputError {
    /// Memory allocation failed (`AVERROR(ENOMEM)`).
    #[error("Memory allocation error")]
    OutOfMemory,

    /// FFmpeg rejected an argument as invalid (`AVERROR(EINVAL)`).
    #[error("Invalid argument provided")]
    InvalidArgument,

    /// The file, URL, or device does not exist (`AVERROR(ENOENT)`).
    #[error("File or stream not found")]
    NotFound,

    /// A low-level I/O error occurred (`AVERROR(EIO)`).
    #[error("I/O error occurred while opening the file or stream")]
    IOError,

    /// The stream or data connection was broken (`AVERROR(EPIPE)`).
    #[error("Pipe error, possibly the stream or data connection was broken")]
    PipeError,

    /// An invalid file descriptor was used (`AVERROR(EBADF)`).
    #[error("Invalid file descriptor")]
    BadFileDescriptor,

    /// The functionality or input format is not supported by the linked
    /// FFmpeg build (`AVERROR(ENOSYS)`).
    #[error("Functionality not implemented or unsupported input format")]
    NotImplemented,

    /// The operation was not permitted (`AVERROR(EPERM)`).
    #[error("Operation not permitted to access the file or stream")]
    OperationNotPermitted,

    /// The file or stream contains invalid or corrupted data
    /// (`AVERROR_INVALIDDATA`).
    #[error("The data in the file or stream is invalid or corrupted")]
    InvalidData,

    /// The connection timed out (`AVERROR(ETIMEDOUT)`).
    #[error("The connection timed out while trying to open the stream")]
    Timeout,

    /// A builder option carried an invalid value (e.g. a non-positive
    /// `set_framerate`, a non-finite `set_ts_scale`, an out-of-range
    /// `set_io_buffer_size`). Setters store values as given and defer
    /// validation to open time, so a bad value surfaces here instead of
    /// panicking in the setter.
    #[error("Invalid input option: {0}")]
    InvalidOption(String),

    /// Any other failure; the payload is the raw FFmpeg error code
    /// (rendered with `av_err2str` in the message).
    #[error("{}. ret:{0}", crate::util::ffmpeg_utils::av_err2str(*.0))]
    UnknownError(i32),

    /// Returned when the input has no usable source: neither a URL nor a
    /// custom read callback was configured.
    #[error("Invalid source provided")]
    InvalidSource,

    /// Returned when the explicitly requested input format name is unknown
    /// to FFmpeg; the payload is the requested name.
    #[error("Invalid source format:{0}")]
    InvalidFormat(String),

    /// Returned when the input requires seeking but the custom input source
    /// provides no seek callback.
    #[error("No seek callback is provided")]
    SeekFunctionMissing,
}

impl From<i32> for OpenInputError {
    fn from(err_code: i32) -> Self {
        match err_code {
            AVERROR_OUT_OF_MEMORY => OpenInputError::OutOfMemory,
            AVERROR_INVALID_ARGUMENT => OpenInputError::InvalidArgument,
            AVERROR_NOT_FOUND => OpenInputError::NotFound,
            AVERROR_IO_ERROR => OpenInputError::IOError,
            AVERROR_PIPE_ERROR => OpenInputError::PipeError,
            AVERROR_BAD_FILE_DESCRIPTOR => OpenInputError::BadFileDescriptor,
            AVERROR_NOT_IMPLEMENTED => OpenInputError::NotImplemented,
            AVERROR_OPERATION_NOT_PERMITTED => OpenInputError::OperationNotPermitted,
            AVERROR_INVALIDDATA => OpenInputError::InvalidData,
            AVERROR_TIMEOUT => OpenInputError::Timeout,
            _ => OpenInputError::UnknownError(err_code),
        }
    }
}

const AVERROR_OUT_OF_MEMORY: i32 = AVERROR(ENOMEM);
const AVERROR_INVALID_ARGUMENT: i32 = AVERROR(EINVAL);
const AVERROR_NOT_FOUND: i32 = AVERROR(ENOENT);
const AVERROR_IO_ERROR: i32 = AVERROR(EIO);
const AVERROR_PIPE_ERROR: i32 = AVERROR(EPIPE);
const AVERROR_BAD_FILE_DESCRIPTOR: i32 = AVERROR(EBADF);
const AVERROR_NOT_IMPLEMENTED: i32 = AVERROR(ENOSYS);
const AVERROR_OPERATION_NOT_PERMITTED: i32 = AVERROR(EPERM);
const AVERROR_PERMISSION_DENIED: i32 = AVERROR(EACCES);
const AVERROR_TIMEOUT: i32 = AVERROR(ETIMEDOUT);
const AVERROR_NOT_SOCKET: i32 = AVERROR(ENOTSOCK);
const AVERROR_AGAIN: i32 = AVERROR(EAGAIN);

/// Errors from probing stream information after an input is opened
/// (`avformat_find_stream_info`).
#[derive(thiserror::Error, Debug)]
#[non_exhaustive]
pub enum FindStreamError {
    /// Memory allocation failed (`AVERROR(ENOMEM)`).
    #[error("Memory allocation error")]
    OutOfMemory,

    /// FFmpeg rejected an argument as invalid (`AVERROR(EINVAL)`).
    #[error("Invalid argument provided")]
    InvalidArgument,

    /// Reached end of file before stream information could be determined
    /// (`AVERROR_EOF`).
    #[error("Reached end of file while looking for stream info")]
    EndOfFile,

    /// The operation timed out (`AVERROR(ETIMEDOUT)`).
    #[error("Timeout occurred while reading stream info")]
    Timeout,

    /// A low-level I/O error occurred (`AVERROR(EIO)`).
    #[error("I/O error occurred while reading stream info")]
    IOError,

    /// The stream contains invalid or corrupted data
    /// (`AVERROR_INVALIDDATA`).
    #[error("The data in the stream is invalid or corrupted")]
    InvalidData,

    /// The functionality or stream format is not supported by the linked
    /// FFmpeg build (`AVERROR(ENOSYS)`).
    #[error("Functionality not implemented or unsupported stream format")]
    NotImplemented,

    /// The operation was not permitted (`AVERROR(EPERM)`).
    #[error("Operation not permitted to access the file or stream")]
    OperationNotPermitted,

    /// Returned when the input contains no streams, or no stream of the
    /// requested kind.
    #[error("No Stream found")]
    NoStreamFound,

    /// Any other failure; the payload is the raw FFmpeg error code
    /// (rendered with `av_err2str` in the message).
    #[error("{}. ret:{0}", crate::util::ffmpeg_utils::av_err2str(*.0))]
    UnknownError(i32),
}

impl From<i32> for FindStreamError {
    fn from(err_code: i32) -> Self {
        match err_code {
            AVERROR_OUT_OF_MEMORY => FindStreamError::OutOfMemory,
            AVERROR_INVALID_ARGUMENT => FindStreamError::InvalidArgument,
            AVERROR_EOF => FindStreamError::EndOfFile,
            AVERROR_TIMEOUT => FindStreamError::Timeout,
            AVERROR_IO_ERROR => FindStreamError::IOError,
            AVERROR_INVALIDDATA => FindStreamError::InvalidData,
            AVERROR_NOT_IMPLEMENTED => FindStreamError::NotImplemented,
            AVERROR_OPERATION_NOT_PERMITTED => FindStreamError::OperationNotPermitted,
            _ => FindStreamError::UnknownError(err_code),
        }
    }
}

/// Errors from parsing a filtergraph description and wiring its inputs and
/// outputs.
#[derive(thiserror::Error, Debug)]
#[non_exhaustive]
pub enum FilterGraphParseError {
    /// Memory allocation failed (`AVERROR(ENOMEM)`).
    #[error("Memory allocation error")]
    OutOfMemory,

    /// FFmpeg rejected an argument as invalid (`AVERROR(EINVAL)`).
    #[error("Invalid argument provided")]
    InvalidArgument,

    /// End of file was reached during parsing (`AVERROR_EOF`).
    #[error("End of file reached during parsing")]
    EndOfFile,

    /// A low-level I/O error occurred (`AVERROR(EIO)`).
    #[error("I/O error occurred during parsing")]
    IOError,

    /// Invalid data was encountered during parsing
    /// (`AVERROR_INVALIDDATA`).
    #[error("Invalid data encountered during parsing")]
    InvalidData,

    /// The functionality or filter is not supported by the linked FFmpeg
    /// build (`AVERROR(ENOSYS)`).
    #[error("Functionality not implemented or unsupported filter format")]
    NotImplemented,

    /// Permission was denied — e.g. by a filter that opens files, such as
    /// `movie=` (`AVERROR(EACCES)`).
    #[error("Permission denied during filter graph parsing")]
    PermissionDenied,

    /// A socket operation was attempted on a non-socket by a filter
    /// touching network resources (`AVERROR(ENOTSOCK)`).
    #[error("Socket operation on non-socket during filter graph parsing")]
    NotSocket,

    /// A filter option named in the description does not exist
    /// (`AVERROR_OPTION_NOT_FOUND`).
    #[error("Option not found during filter graph configuration")]
    OptionNotFound,

    /// Returned when a stream reference in the filtergraph description
    /// names an input file index that does not exist; fields are the index
    /// and the description.
    #[error("Invalid file index {0} in filtergraph description {1}")]
    InvalidFileIndexInFg(usize, String),

    /// Returned when an output URL references an input file index that does
    /// not exist; fields are the index and the URL.
    #[error("Invalid file index {0} in output url: {1}")]
    InvalidFileIndexInOutput(usize, String),

    /// Returned when a stream specifier in the filtergraph description is
    /// malformed; the payload is the offending text.
    #[error("Invalid filter specifier {0}")]
    InvalidFilterSpecifier(String),

    /// Returned when a filtergraph output pad is not connected to any
    /// output; fields are the filter name, the pad index, and its link
    /// label.
    #[error("Filter '{0}' has output {1} ({2}) unconnected")]
    OutputUnconnected(String, usize, String),

    /// Any other failure; the payload is the raw FFmpeg error code.
    #[error("An unknown error occurred. ret: {0}")]
    UnknownError(i32),
}

impl From<i32> for FilterGraphParseError {
    fn from(err_code: i32) -> Self {
        match err_code {
            AVERROR_OUT_OF_MEMORY => FilterGraphParseError::OutOfMemory,
            AVERROR_INVALID_ARGUMENT => FilterGraphParseError::InvalidArgument,
            AVERROR_EOF => FilterGraphParseError::EndOfFile,
            AVERROR_IO_ERROR => FilterGraphParseError::IOError,
            AVERROR_INVALIDDATA => FilterGraphParseError::InvalidData,
            AVERROR_NOT_IMPLEMENTED => FilterGraphParseError::NotImplemented,
            AVERROR_OPTION_NOT_FOUND => FilterGraphParseError::OptionNotFound,
            // EACCES/ENOTSOCK reach here from filters that touch files or
            // sockets (e.g. `movie=`); map them to the variants this enum
            // already declares instead of degrading to UnknownError.
            AVERROR_PERMISSION_DENIED => FilterGraphParseError::PermissionDenied,
            AVERROR_NOT_SOCKET => FilterGraphParseError::NotSocket,
            _ => FilterGraphParseError::UnknownError(err_code),
        }
    }
}

/// Errors from allocating an output format context
/// (`avformat_alloc_output_context2`).
#[derive(thiserror::Error, Debug)]
#[non_exhaustive]
pub enum AllocOutputContextError {
    /// Memory allocation failed (`AVERROR(ENOMEM)`).
    #[error("Memory allocation error")]
    OutOfMemory,

    /// FFmpeg rejected an argument as invalid (`AVERROR(EINVAL)`).
    #[error("Invalid argument provided")]
    InvalidArgument,

    /// The file or stream does not exist (`AVERROR(ENOENT)`).
    #[error("File or stream not found")]
    NotFound,

    /// A low-level I/O error occurred (`AVERROR(EIO)`).
    #[error("I/O error occurred while allocating the output context")]
    IOError,

    /// The stream or data connection was broken (`AVERROR(EPIPE)`).
    #[error("Pipe error, possibly the stream or data connection was broken")]
    PipeError,

    /// An invalid file descriptor was used (`AVERROR(EBADF)`).
    #[error("Invalid file descriptor")]
    BadFileDescriptor,

    /// The functionality or output format is not supported by the linked
    /// FFmpeg build (`AVERROR(ENOSYS)`).
    #[error("Functionality not implemented or unsupported output format")]
    NotImplemented,

    /// The operation was not permitted (`AVERROR(EPERM)`).
    #[error("Operation not permitted to allocate the output context")]
    OperationNotPermitted,

    /// Permission was denied (`AVERROR(EACCES)`).
    #[error("Permission denied while allocating the output context")]
    PermissionDenied,

    /// The operation timed out (`AVERROR(ETIMEDOUT)`).
    #[error("The connection timed out while trying to allocate the output context")]
    Timeout,

    /// Any other failure; the payload is the raw FFmpeg error code
    /// (rendered with `av_err2str` in the message).
    #[error("{}. ret:{0}", crate::util::ffmpeg_utils::av_err2str(*.0))]
    UnknownError(i32),
}

impl From<i32> for AllocOutputContextError {
    fn from(err_code: i32) -> Self {
        match err_code {
            AVERROR_OUT_OF_MEMORY => AllocOutputContextError::OutOfMemory,
            AVERROR_INVALID_ARGUMENT => AllocOutputContextError::InvalidArgument,
            AVERROR_NOT_FOUND => AllocOutputContextError::NotFound,
            AVERROR_IO_ERROR => AllocOutputContextError::IOError,
            AVERROR_PIPE_ERROR => AllocOutputContextError::PipeError,
            AVERROR_BAD_FILE_DESCRIPTOR => AllocOutputContextError::BadFileDescriptor,
            AVERROR_NOT_IMPLEMENTED => AllocOutputContextError::NotImplemented,
            AVERROR_OPERATION_NOT_PERMITTED => AllocOutputContextError::OperationNotPermitted,
            AVERROR_PERMISSION_DENIED => AllocOutputContextError::PermissionDenied,
            AVERROR_TIMEOUT => AllocOutputContextError::Timeout,
            _ => AllocOutputContextError::UnknownError(err_code),
        }
    }
}

/// Errors from opening and configuring an output: resolving formats and
/// encoders, mapping streams, validating options, and opening the target
/// for writing.
#[derive(thiserror::Error, Debug)]
#[non_exhaustive]
pub enum OpenOutputError {
    /// Memory allocation failed (`AVERROR(ENOMEM)`).
    #[error("Memory allocation error")]
    OutOfMemory,

    /// FFmpeg rejected an argument as invalid (`AVERROR(EINVAL)`).
    #[error("Invalid argument provided")]
    InvalidArgument,

    /// The file or stream does not exist (`AVERROR(ENOENT)`).
    #[error("File or stream not found")]
    NotFound,

    /// A low-level I/O error occurred (`AVERROR(EIO)`).
    #[error("I/O error occurred while opening the file or stream")]
    IOError,

    /// The stream or data connection was broken (`AVERROR(EPIPE)`).
    #[error("Pipe error, possibly the stream or data connection was broken")]
    PipeError,

    /// An invalid file descriptor was used (`AVERROR(EBADF)`).
    #[error("Invalid file descriptor")]
    BadFileDescriptor,

    /// The functionality or output format is not supported by the linked
    /// FFmpeg build (`AVERROR(ENOSYS)`).
    #[error("Functionality not implemented or unsupported output format")]
    NotImplemented,

    /// The operation was not permitted (`AVERROR(EPERM)`).
    #[error("Operation not permitted to open the file or stream")]
    OperationNotPermitted,

    /// Permission was denied (`AVERROR(EACCES)`).
    #[error("Permission denied while opening the file or stream")]
    PermissionDenied,

    /// The operation timed out (`AVERROR(ETIMEDOUT)`).
    #[error("The connection timed out while trying to open the file or stream")]
    Timeout,

    /// No encoder was found for the requested codec
    /// (`AVERROR_ENCODER_NOT_FOUND`).
    #[error("encoder not found")]
    EncoderNotFound,

    /// A named encoder could not be opened because the linked FFmpeg build
    /// does not provide it — either it was compiled without that encoder
    /// (e.g. no `--enable-libx264`) or the name is not a known encoder at all.
    /// Unlike the bare [`EncoderNotFound`](Self::EncoderNotFound) errno
    /// mapping, this names the encoder so the fix is actionable. `name` is the
    /// encoder the caller requested, or the codec the output format guessed
    /// when none was set explicitly.
    #[error(
        "encoder '{name}' is not available in the linked FFmpeg build — link \
         an FFmpeg build that provides it (for example one configured with \
         --enable-libx264 for libx264), or select a different encoder via \
         Output::set_video_codec / set_audio_codec / set_subtitle_codec \
         (list what the build provides with codec::get_encoders)"
    )]
    EncoderUnavailable {
        /// The requested encoder name, or the format's guessed default
        /// codec when none was set explicitly.
        name: String,
    },

    /// Returned when a stream map specifier matches no streams; the payload
    /// is the specifier.
    #[error("Stream map '{0}' matches no streams;")]
    MatchesNoStreams(String),

    /// A stream map combined stream copy with a per-map re-encoding
    /// request ([`StreamMap::codec`] / [`StreamMap::codec_opt`]): copied
    /// packets never pass through an encoder, so a per-map codec or
    /// per-map codec options could never take effect. Raised at `build()`
    /// instead of silently ignoring the request (the FFmpeg CLI merely
    /// warns about such unused options).
    ///
    /// [`StreamMap::codec`]: crate::core::context::output::StreamMap::codec
    /// [`StreamMap::codec_opt`]: crate::core::context::output::StreamMap::codec_opt
    #[error(
        "stream map '{spec}' requests stream copy together with {what}; \
         stream copy and per-map re-encoding settings are mutually exclusive"
    )]
    StreamMapCopyConflict {
        /// The offending stream map specifier.
        spec: String,
        /// The per-map re-encoding setting that conflicts with copy.
        what: &'static str,
    },

    /// Returned when an output references an invalid filtergraph link
    /// label; the payload is the label.
    #[error("Invalid label {0}")]
    InvalidLabel(String),

    /// Returned when the output ends up with no streams at all.
    #[error("not contain any stream")]
    NotContainStream,

    /// Returned when the format of the frame feeding an output stream is
    /// unknown, so encoder parameters cannot be derived from it.
    #[error("unknown format of the frame")]
    UnknownFrameFormat,

    /// Returned when an input URL references a file index that does not
    /// exist; fields are the index and the URL.
    #[error("Invalid file index {0} in input url: {1}")]
    InvalidFileIndexInIntput(usize, String),

    /// Any other failure; the payload is the raw FFmpeg error code
    /// (rendered with `av_err2str` in the message).
    #[error("{}. ret:{0}", crate::util::ffmpeg_utils::av_err2str(*.0))]
    UnknownError(i32),

    /// Returned when the output has no usable destination: neither a URL
    /// nor a custom write callback was configured.
    #[error("Invalid sink provided")]
    InvalidSink,

    /// Returned when the output format requires seeking but the custom
    /// output sink provides no seek callback.
    #[error("No seek callback is provided")]
    SeekFunctionMissing,

    /// Returned when the requested output format name is unknown to FFmpeg;
    /// the payload is the requested name.
    #[error("Format '{0}' is unsupported")]
    FormatUnsupported(String),

    /// Returned when a pixel format name is not recognized; the payload is
    /// the name.
    #[error("Unknown pixel format: '{0}'")]
    UnknownPixelFormat(String),

    /// Returned when a sample format name is not recognized; the payload is
    /// the name.
    #[error("Unknown sample format: '{0}'")]
    UnknownSampleFormat(String),

    /// A builder option carried an invalid value (e.g. a malformed
    /// `set_force_key_frames` spec, an out-of-range `set_io_buffer_size`).
    /// Setters store values as given and defer validation to open time, so
    /// a bad value surfaces here instead of panicking in the setter or
    /// forcing a `Result` into the middle of a builder chain.
    #[error("Invalid output option: {0}")]
    InvalidOption(String),

    /// Returned when reading an attachment file fails; the payload is the
    /// path, with the underlying I/O error as the source.
    #[error("Failed to read attachment file '{0}'")]
    AttachmentRead(String, #[source] io::Error),

    /// Returned when an attachment file is empty; the payload is the path.
    #[error("Attachment file '{0}' is empty")]
    AttachmentEmpty(String),

    /// Returned when an attachment file exceeds the size limit; fields are
    /// the path, its size in bytes, and the limit in bytes.
    #[error("Attachment file '{0}' is too large ({1} bytes, limit {2} bytes)")]
    AttachmentTooLarge(String, u64, u64),

    /// Returned when an attachment was configured with an empty mimetype;
    /// the payload is the file path.
    #[error("Attachment mimetype must not be empty (file '{0}')")]
    AttachmentEmptyMimetype(String),

    /// A per-output video filter ([`Output::set_video_filter`]) was combined
    /// with stream copy for the same output's video — either
    /// `set_video_codec("copy")` or a copy stream map covering a video
    /// stream. Mirrors the FFmpeg CLI error for `-vf` + `-c:v copy`
    /// ("Filtering and streamcopy cannot be used together",
    /// ffmpeg_mux_init.c streamcopy_init).
    ///
    /// [`Output::set_video_filter`]: crate::core::context::output::Output::set_video_filter
    #[error(
        "Filtergraph '{0}' was specified, but codec copy was selected for the \
         output's video stream. Filtering and streamcopy cannot be used together"
    )]
    FilterWithStreamCopy(String),

    /// A per-output video filter ([`Output::set_video_filter`]) was set on an
    /// output whose video stream is fed by a context-level filtergraph
    /// (`FfmpegContextBuilder::filter_desc`). Mirrors the FFmpeg CLI error for
    /// `-vf` + `-filter_complex` on the same stream (ffmpeg_mux_init.c
    /// ost_get_filters: "Simple and complex filtering cannot be used together
    /// for the same stream").
    ///
    /// [`Output::set_video_filter`]: crate::core::context::output::Output::set_video_filter
    #[error(
        "Filtergraph '{0}' was specified for a video stream fed from a \
         context-level filtergraph. Simple and complex filtering cannot be \
         used together for the same stream"
    )]
    SimpleAndComplexFilter(String),

    /// A per-output simple filtergraph must be one connected linear chain:
    /// exactly one video input pad, one video output pad, a single connected
    /// component, and a directed path from the input to the output (fftools
    /// fg_create_simple's contract plus the topology rules a simple graph
    /// implies — a disconnected or unreachable description would encode
    /// unrelated frames or hang instead of filtering the stream). The path
    /// requirement is structural: the input pad must be wired into the flow
    /// that feeds the output pad, while a filter that may discard it at
    /// runtime (`streamselect` whose applied `map` selects another input —
    /// rewritable mid-stream via `sendcmd`) is accepted, matching the CLI.
    /// `reason` names the violated rule. Descriptions that split, merge or
    /// source streams belong in the context-level `filter_desc`.
    #[error(
        "Simple filtergraph '{desc}' is not a single connected chain: {reason}; \
         use FfmpegContextBuilder::filter_desc for complex graphs"
    )]
    SimpleFilterInvalidShape {
        /// The offending filtergraph description, as configured.
        desc: String,
        /// The topology rule the description violates.
        reason: String,
    },

    /// A configured [`Output::set_video_filter`] chain that no re-encoded
    /// video stream ended up consuming: the output has no video stream at all
    /// (audio-only input, `disable_video()`, or maps that matched no video
    /// stream). The ffmpeg CLI silently ignores `-vf` in that situation; the
    /// crate refuses instead of dropping configuration on the floor.
    ///
    /// [`Output::set_video_filter`]: crate::core::context::output::Output::set_video_filter
    #[error(
        "video filter '{0}' was configured, but the output ended up with no \
         re-encoded video stream to run it (audio-only input, disable_video(), \
         or maps matching no video stream); remove the filter or map a video \
         stream"
    )]
    VideoFilterUnused(String),

    /// A per-output simple filtergraph's pads must match the stream's media
    /// type (fftools fg_create_simple: "Filtergraph has a %s output, cannot
    /// connect it to %s output stream") — e.g. an audio chain like `anull`
    /// cannot be attached as a video filter.
    ///
    /// The media-type labels are static (`"video"`, `"audio"`, ... — the
    /// strings fftools prints), which keeps this variant inside `Error`'s
    /// 64-byte layout; three owned `String`s would grow every hot-path
    /// `Result` in the crate.
    #[error(
        "Simple filtergraph '{desc}' has a {found} pad, cannot connect it to \
         the {expected} stream of this output"
    )]
    SimpleFilterMediaTypeMismatch {
        /// The offending filtergraph description, as configured.
        desc: String,
        /// The media type of the mismatched pad.
        found: &'static str,
        /// The media type the output stream requires.
        expected: &'static str,
    },
}

impl From<i32> for OpenOutputError {
    fn from(err_code: i32) -> Self {
        match err_code {
            AVERROR_OUT_OF_MEMORY => OpenOutputError::OutOfMemory,
            AVERROR_INVALID_ARGUMENT => OpenOutputError::InvalidArgument,
            AVERROR_NOT_FOUND => OpenOutputError::NotFound,
            AVERROR_IO_ERROR => OpenOutputError::IOError,
            AVERROR_PIPE_ERROR => OpenOutputError::PipeError,
            AVERROR_BAD_FILE_DESCRIPTOR => OpenOutputError::BadFileDescriptor,
            AVERROR_NOT_IMPLEMENTED => OpenOutputError::NotImplemented,
            AVERROR_OPERATION_NOT_PERMITTED => OpenOutputError::OperationNotPermitted,
            AVERROR_PERMISSION_DENIED => OpenOutputError::PermissionDenied,
            AVERROR_TIMEOUT => OpenOutputError::Timeout,
            AVERROR_ENCODER_NOT_FOUND => OpenOutputError::EncoderNotFound,
            _ => OpenOutputError::UnknownError(err_code),
        }
    }
}

/// Errors from enumerating capture devices (cameras, microphones,
/// screens).
#[derive(thiserror::Error, Debug)]
#[non_exhaustive]
pub enum FindDevicesError {
    /// Returned on macOS when the `AVCaptureDevice` class is not available.
    #[error("AVCaptureDevice class not found in macOS")]
    AVCaptureDeviceNotFound,

    /// Returned when device enumeration for the requested media type is not
    /// supported; the payload is the raw `AVMediaType` value.
    #[error("current media_type({0}) is not supported")]
    MediaTypeSupported(i32),
    /// Returned when device enumeration is not supported on the current
    /// operating system.
    #[error("current OS is not supported")]
    OsNotSupported,
    /// Returned when a device description could not be converted to a UTF-8
    /// string.
    #[error("device_description can not to string")]
    UTF8Error,

    /// Memory allocation failed (`AVERROR(ENOMEM)`).
    #[error("Memory allocation error")]
    OutOfMemory,
    /// FFmpeg rejected an argument as invalid (`AVERROR(EINVAL)`).
    #[error("Invalid argument provided")]
    InvalidArgument,
    /// The device or stream does not exist (`AVERROR(ENOENT)`).
    #[error("Device or stream not found")]
    NotFound,
    /// A low-level I/O error occurred (`AVERROR(EIO)`).
    #[error("I/O error occurred while accessing the device or stream")]
    IOError,
    /// The operation was not permitted (`AVERROR(EPERM)`).
    #[error("Operation not permitted for this device or stream")]
    OperationNotPermitted,
    /// Permission was denied (`AVERROR(EACCES)`).
    #[error("Permission denied while accessing the device or stream")]
    PermissionDenied,
    /// The functionality is not supported by the linked FFmpeg build
    /// (`AVERROR(ENOSYS)`).
    #[error("This functionality is not implemented")]
    NotImplemented,
    /// An invalid file descriptor was used (`AVERROR(EBADF)`).
    #[error("Bad file descriptor")]
    BadFileDescriptor,
    /// Any other failure; the payload is the raw FFmpeg error code
    /// (rendered with `av_err2str` in the message).
    #[error("{}. ret:{0}", crate::util::ffmpeg_utils::av_err2str(*.0))]
    UnknownError(i32),
}

impl From<i32> for FindDevicesError {
    fn from(err_code: i32) -> Self {
        match err_code {
            AVERROR_OUT_OF_MEMORY => FindDevicesError::OutOfMemory,
            AVERROR_INVALID_ARGUMENT => FindDevicesError::InvalidArgument,
            AVERROR_NOT_FOUND => FindDevicesError::NotFound,
            AVERROR_IO_ERROR => FindDevicesError::IOError,
            AVERROR_OPERATION_NOT_PERMITTED => FindDevicesError::OperationNotPermitted,
            AVERROR_PERMISSION_DENIED => FindDevicesError::PermissionDenied,
            AVERROR_NOT_IMPLEMENTED => FindDevicesError::NotImplemented,
            AVERROR_BAD_FILE_DESCRIPTOR => FindDevicesError::BadFileDescriptor,
            _ => FindDevicesError::UnknownError(err_code),
        }
    }
}

/// Errors from writing the output container header
/// (`avformat_write_header`), carried by
/// [`MuxingOperationError::WriteHeader`].
#[derive(thiserror::Error, Debug)]
#[non_exhaustive]
pub enum WriteHeaderError {
    /// Memory allocation failed (`AVERROR(ENOMEM)`).
    #[error("Memory allocation error")]
    OutOfMemory,

    /// FFmpeg rejected an argument as invalid (`AVERROR(EINVAL)`).
    #[error("Invalid argument provided")]
    InvalidArgument,

    /// The file or stream does not exist (`AVERROR(ENOENT)`).
    #[error("File or stream not found")]
    NotFound,

    /// A low-level I/O error occurred (`AVERROR(EIO)`).
    #[error("I/O error occurred while writing the header")]
    IOError,

    /// The stream or data connection was broken (`AVERROR(EPIPE)`).
    #[error("Pipe error, possibly the stream or data connection was broken")]
    PipeError,

    /// An invalid file descriptor was used (`AVERROR(EBADF)`).
    #[error("Invalid file descriptor")]
    BadFileDescriptor,

    /// The functionality or output format is not supported by the linked
    /// FFmpeg build (`AVERROR(ENOSYS)`).
    #[error("Functionality not implemented or unsupported output format")]
    NotImplemented,

    /// The operation was not permitted (`AVERROR(EPERM)`).
    #[error("Operation not permitted to write the header")]
    OperationNotPermitted,

    /// Permission was denied (`AVERROR(EACCES)`).
    #[error("Permission denied while writing the header")]
    PermissionDenied,

    /// The operation timed out (`AVERROR(ETIMEDOUT)`).
    #[error("The connection timed out while trying to write the header")]
    Timeout,

    /// Any other failure; the payload is the raw FFmpeg error code
    /// (rendered with `av_err2str` in the message).
    #[error("{}. ret:{0}", crate::util::ffmpeg_utils::av_err2str(*.0))]
    UnknownError(i32),
}

impl From<i32> for WriteHeaderError {
    fn from(err_code: i32) -> Self {
        match err_code {
            AVERROR_OUT_OF_MEMORY => WriteHeaderError::OutOfMemory,
            AVERROR_INVALID_ARGUMENT => WriteHeaderError::InvalidArgument,
            AVERROR_NOT_FOUND => WriteHeaderError::NotFound,
            AVERROR_IO_ERROR => WriteHeaderError::IOError,
            AVERROR_PIPE_ERROR => WriteHeaderError::PipeError,
            AVERROR_BAD_FILE_DESCRIPTOR => WriteHeaderError::BadFileDescriptor,
            AVERROR_NOT_IMPLEMENTED => WriteHeaderError::NotImplemented,
            AVERROR_OPERATION_NOT_PERMITTED => WriteHeaderError::OperationNotPermitted,
            AVERROR_PERMISSION_DENIED => WriteHeaderError::PermissionDenied,
            AVERROR_TIMEOUT => WriteHeaderError::Timeout,
            _ => WriteHeaderError::UnknownError(err_code),
        }
    }
}

/// Errors from encoding a subtitle (`avcodec_encode_subtitle`).
#[derive(thiserror::Error, Debug)]
#[non_exhaustive]
pub enum EncodeSubtitleError {
    /// Memory allocation failed (`AVERROR(ENOMEM)`).
    #[error("Memory allocation error while encoding subtitle")]
    OutOfMemory,

    /// FFmpeg rejected an argument as invalid (`AVERROR(EINVAL)`).
    #[error("Invalid argument provided for subtitle encoding")]
    InvalidArgument,

    /// The operation was not permitted (`AVERROR(EPERM)`).
    #[error("Operation not permitted while encoding subtitle")]
    OperationNotPermitted,

    /// Subtitle encoding is not supported by the linked FFmpeg build
    /// (`AVERROR(ENOSYS)`).
    #[error("The encoding functionality is not implemented or unsupported")]
    NotImplemented,

    /// The encoder is temporarily unable to accept input
    /// (`AVERROR(EAGAIN)`); retry later.
    #[error("Encoder temporarily unable to process, please retry")]
    TryAgain,

    /// Any other failure; the payload is the raw FFmpeg error code.
    #[error("Subtitle encoding failed with unknown error. ret: {0}")]
    UnknownError(i32),
}

impl From<i32> for EncodeSubtitleError {
    fn from(err_code: i32) -> Self {
        match err_code {
            AVERROR_OUT_OF_MEMORY => EncodeSubtitleError::OutOfMemory,
            AVERROR_INVALID_ARGUMENT => EncodeSubtitleError::InvalidArgument,
            AVERROR_OPERATION_NOT_PERMITTED => EncodeSubtitleError::OperationNotPermitted,
            AVERROR_NOT_IMPLEMENTED => EncodeSubtitleError::NotImplemented,
            AVERROR_AGAIN => EncodeSubtitleError::TryAgain,
            _ => EncodeSubtitleError::UnknownError(err_code),
        }
    }
}

/// Errors from allocating an `AVPacket`.
#[derive(thiserror::Error, Debug)]
#[non_exhaustive]
pub enum AllocPacketError {
    /// Packet allocation failed (`av_packet_alloc` returned no packet).
    #[error("Memory allocation error while alloc packet")]
    OutOfMemory,
}

/// Errors from allocating an `AVFrame`.
#[derive(thiserror::Error, Debug)]
#[non_exhaustive]
pub enum AllocFrameError {
    /// Frame allocation failed (`av_frame_alloc` returned no frame).
    #[error("Memory allocation error while alloc frame")]
    OutOfMemory,
}

/// Errors from [`make_frame_writable`], the safe wrapper over FFmpeg's
/// `av_frame_make_writable`: ensuring exclusive ownership of a frame's data
/// buffers may allocate new buffers and copy into them, and that underlying
/// call can fail. Common AVERROR codes map to named variants; anything else
/// carries the raw code.
///
/// [`make_frame_writable`]: crate::util::ffmpeg_utils::make_frame_writable
#[derive(thiserror::Error, Debug)]
#[non_exhaustive]
pub enum FrameWritableError {
    /// Allocating or copying the frame's data buffers failed
    /// (`AVERROR(ENOMEM)`).
    #[error("Memory allocation error while copying frame data")]
    OutOfMemory,

    /// FFmpeg rejected the frame as invalid (`AVERROR(EINVAL)`).
    #[error("Invalid argument provided")]
    InvalidArgument,

    /// Any other failure; the payload is the raw FFmpeg error code
    /// (rendered with `av_err2str` in the message).
    #[error("{}. ret:{0}", crate::util::ffmpeg_utils::av_err2str(*.0))]
    UnknownError(i32),
}

impl From<i32> for FrameWritableError {
    fn from(err_code: i32) -> Self {
        match err_code {
            AVERROR_OUT_OF_MEMORY => FrameWritableError::OutOfMemory,
            AVERROR_INVALID_ARGUMENT => FrameWritableError::InvalidArgument,
            _ => FrameWritableError::UnknownError(err_code),
        }
    }
}

/// FFmpeg-level errors during muxing, carried by [`MuxingOperationError`].
#[derive(thiserror::Error, Debug)]
#[non_exhaustive]
pub enum MuxingError {
    /// Memory allocation failed (`AVERROR(ENOMEM)`).
    #[error("Memory allocation error")]
    OutOfMemory,

    /// FFmpeg rejected an argument as invalid (`AVERROR(EINVAL)`).
    #[error("Invalid argument provided")]
    InvalidArgument,

    /// A low-level I/O error occurred (`AVERROR(EIO)`).
    #[error("I/O error occurred during muxing")]
    IOError,

    /// The stream or data connection was broken (`AVERROR(EPIPE)`).
    #[error("Broken pipe during muxing")]
    PipeError,

    /// An invalid file descriptor was used (`AVERROR(EBADF)`).
    #[error("Bad file descriptor encountered")]
    BadFileDescriptor,

    /// The functionality is not supported by the linked FFmpeg build
    /// (`AVERROR(ENOSYS)`).
    #[error("Functionality not implemented or unsupported")]
    NotImplemented,

    /// The operation was not permitted (`AVERROR(EPERM)`).
    #[error("Operation not permitted")]
    OperationNotPermitted,

    /// The resource is temporarily unavailable (`AVERROR(EAGAIN)`).
    #[error("Resource temporarily unavailable")]
    TryAgain,

    /// Any other failure; the payload is the raw FFmpeg error code
    /// (rendered with `av_err2str` in the message).
    #[error("{}. ret:{0}", crate::util::ffmpeg_utils::av_err2str(*.0))]
    UnknownError(i32),
}

impl From<i32> for MuxingError {
    fn from(err_code: i32) -> Self {
        match err_code {
            AVERROR_OUT_OF_MEMORY => MuxingError::OutOfMemory,
            AVERROR_INVALID_ARGUMENT => MuxingError::InvalidArgument,
            AVERROR_IO_ERROR => MuxingError::IOError,
            AVERROR_PIPE_ERROR => MuxingError::PipeError,
            AVERROR_BAD_FILE_DESCRIPTOR => MuxingError::BadFileDescriptor,
            AVERROR_NOT_IMPLEMENTED => MuxingError::NotImplemented,
            AVERROR_OPERATION_NOT_PERMITTED => MuxingError::OperationNotPermitted,
            AVERROR_AGAIN => MuxingError::TryAgain,
            _ => MuxingError::UnknownError(err_code),
        }
    }
}

/// FFmpeg-level errors while opening an encoder, carried by
/// [`OpenEncoderOperationError`].
#[derive(thiserror::Error, Debug)]
#[non_exhaustive]
pub enum OpenEncoderError {
    /// Memory allocation failed (`AVERROR(ENOMEM)`).
    #[error("Memory allocation error occurred during encoder initialization")]
    OutOfMemory,

    /// FFmpeg rejected an argument as invalid (`AVERROR(EINVAL)`).
    #[error("Invalid argument provided to encoder")]
    InvalidArgument,

    /// A low-level I/O error occurred (`AVERROR(EIO)`).
    #[error("I/O error occurred while opening encoder")]
    IOError,

    /// The stream or data connection was broken (`AVERROR(EPIPE)`).
    #[error("Broken pipe encountered during encoder initialization")]
    PipeError,

    /// An invalid file descriptor was used (`AVERROR(EBADF)`).
    #[error("Bad file descriptor used in encoder")]
    BadFileDescriptor,

    /// The functionality is not supported by the linked FFmpeg build
    /// (`AVERROR(ENOSYS)`).
    #[error("Encoder functionality not implemented or unsupported")]
    NotImplemented,

    /// The operation was not permitted (`AVERROR(EPERM)`).
    #[error("Operation not permitted while configuring encoder")]
    OperationNotPermitted,

    /// The resource is temporarily unavailable (`AVERROR(EAGAIN)`).
    #[error("Resource temporarily unavailable during encoder setup")]
    TryAgain,

    /// Any other failure; the payload is the raw FFmpeg error code.
    #[error("An unknown error occurred in encoder setup. ret:{0}")]
    UnknownError(i32),
}

impl From<i32> for OpenEncoderError {
    fn from(err_code: i32) -> Self {
        match err_code {
            AVERROR_OUT_OF_MEMORY => OpenEncoderError::OutOfMemory,
            AVERROR_INVALID_ARGUMENT => OpenEncoderError::InvalidArgument,
            AVERROR_IO_ERROR => OpenEncoderError::IOError,
            AVERROR_PIPE_ERROR => OpenEncoderError::PipeError,
            AVERROR_BAD_FILE_DESCRIPTOR => OpenEncoderError::BadFileDescriptor,
            AVERROR_NOT_IMPLEMENTED => OpenEncoderError::NotImplemented,
            AVERROR_OPERATION_NOT_PERMITTED => OpenEncoderError::OperationNotPermitted,
            AVERROR_AGAIN => OpenEncoderError::TryAgain,
            _ => OpenEncoderError::UnknownError(err_code),
        }
    }
}

/// FFmpeg-level errors during encoding, carried by
/// [`EncodingOperationError`].
#[derive(thiserror::Error, Debug)]
#[non_exhaustive]
pub enum EncodingError {
    /// Memory allocation failed (`AVERROR(ENOMEM)`).
    #[error("Memory allocation error during encoding")]
    OutOfMemory,

    /// FFmpeg rejected an argument as invalid (`AVERROR(EINVAL)`).
    #[error("Invalid argument provided to encoder")]
    InvalidArgument,

    /// A low-level I/O error occurred (`AVERROR(EIO)`).
    #[error("I/O error occurred during encoding")]
    IOError,

    /// The stream or data connection was broken (`AVERROR(EPIPE)`).
    #[error("Broken pipe encountered during encoding")]
    PipeError,

    /// An invalid file descriptor was used (`AVERROR(EBADF)`).
    #[error("Bad file descriptor encountered during encoding")]
    BadFileDescriptor,

    /// The functionality is not supported by the linked FFmpeg build
    /// (`AVERROR(ENOSYS)`).
    #[error("Functionality not implemented or unsupported encoding feature")]
    NotImplemented,

    /// The operation was not permitted (`AVERROR(EPERM)`).
    #[error("Operation not permitted for encoder")]
    OperationNotPermitted,

    /// The encoder is temporarily unable to accept or produce data
    /// (`AVERROR(EAGAIN)`).
    #[error("Resource temporarily unavailable, try again later")]
    TryAgain,

    /// The encoder reached end of stream; no more packets will be produced
    /// (`AVERROR_EOF`).
    #[error("End of stream reached or no more frames to encode")]
    EndOfStream,

    /// Any other failure; the payload is the raw FFmpeg error code.
    #[error("An unknown error occurred during encoding. ret: {0}")]
    UnknownError(i32),
}

impl From<i32> for EncodingError {
    fn from(err_code: i32) -> Self {
        match err_code {
            AVERROR_OUT_OF_MEMORY => EncodingError::OutOfMemory,
            AVERROR_INVALID_ARGUMENT => EncodingError::InvalidArgument,
            AVERROR_IO_ERROR => EncodingError::IOError,
            AVERROR_PIPE_ERROR => EncodingError::PipeError,
            AVERROR_BAD_FILE_DESCRIPTOR => EncodingError::BadFileDescriptor,
            AVERROR_NOT_IMPLEMENTED => EncodingError::NotImplemented,
            AVERROR_OPERATION_NOT_PERMITTED => EncodingError::OperationNotPermitted,
            AVERROR_AGAIN => EncodingError::TryAgain,
            AVERROR_EOF => EncodingError::EndOfStream,
            _ => EncodingError::UnknownError(err_code),
        }
    }
}

/// FFmpeg-level errors during filtergraph processing, carried by
/// [`FilterGraphOperationError`].
#[derive(thiserror::Error, Debug)]
#[non_exhaustive]
pub enum FilterGraphError {
    /// Memory allocation failed (`AVERROR(ENOMEM)`).
    #[error("Memory allocation error during filter graph processing")]
    OutOfMemory,

    /// FFmpeg rejected an argument as invalid (`AVERROR(EINVAL)`).
    #[error("Invalid argument provided to filter graph processing")]
    InvalidArgument,

    /// A low-level I/O error occurred (`AVERROR(EIO)`).
    #[error("I/O error occurred during filter graph processing")]
    IOError,

    /// The stream or data connection was broken (`AVERROR(EPIPE)`).
    #[error("Broken pipe during filter graph processing")]
    PipeError,

    /// An invalid file descriptor was used (`AVERROR(EBADF)`).
    #[error("Bad file descriptor encountered during filter graph processing")]
    BadFileDescriptor,

    /// The functionality is not supported by the linked FFmpeg build
    /// (`AVERROR(ENOSYS)`).
    #[error("Functionality not implemented or unsupported during filter graph processing")]
    NotImplemented,

    /// The operation was not permitted (`AVERROR(EPERM)`).
    #[error("Operation not permitted during filter graph processing")]
    OperationNotPermitted,

    /// The graph is temporarily unable to accept or produce data
    /// (`AVERROR(EAGAIN)`).
    #[error("Resource temporarily unavailable during filter graph processing")]
    TryAgain,

    /// The filtergraph reached end of stream (`AVERROR_EOF`).
    #[error("EOF")]
    EOF,

    /// Any other failure; the payload is the raw FFmpeg error code.
    #[error("An unknown error occurred during filter graph processing. ret:{0}")]
    UnknownError(i32),
}

impl From<i32> for FilterGraphError {
    fn from(err_code: i32) -> Self {
        match err_code {
            AVERROR_OUT_OF_MEMORY => FilterGraphError::OutOfMemory,
            AVERROR_INVALID_ARGUMENT => FilterGraphError::InvalidArgument,
            AVERROR_IO_ERROR => FilterGraphError::IOError,
            AVERROR_PIPE_ERROR => FilterGraphError::PipeError,
            AVERROR_BAD_FILE_DESCRIPTOR => FilterGraphError::BadFileDescriptor,
            AVERROR_NOT_IMPLEMENTED => FilterGraphError::NotImplemented,
            AVERROR_OPERATION_NOT_PERMITTED => FilterGraphError::OperationNotPermitted,
            AVERROR_AGAIN => FilterGraphError::TryAgain,
            AVERROR_EOF => FilterGraphError::EOF,
            _ => FilterGraphError::UnknownError(err_code),
        }
    }
}

/// FFmpeg-level errors while opening a decoder, carried by
/// [`OpenDecoderOperationError`].
#[derive(thiserror::Error, Debug)]
#[non_exhaustive]
pub enum OpenDecoderError {
    /// Memory allocation failed (`AVERROR(ENOMEM)`).
    #[error("Memory allocation error during decoder initialization")]
    OutOfMemory,

    /// FFmpeg rejected an argument as invalid (`AVERROR(EINVAL)`).
    #[error("Invalid argument provided during decoder initialization")]
    InvalidArgument,

    /// The functionality is not supported by the linked FFmpeg build
    /// (`AVERROR(ENOSYS)`).
    #[error("Functionality not implemented or unsupported during decoder initialization")]
    NotImplemented,

    /// The resource is temporarily unavailable (`AVERROR(EAGAIN)`).
    #[error("Resource temporarily unavailable during decoder initialization")]
    TryAgain,

    /// A low-level I/O error occurred (`AVERROR(EIO)`).
    #[error("I/O error occurred during decoder initialization")]
    IOError,

    /// Any other failure; the payload is the raw FFmpeg error code.
    #[error("An unknown error occurred during decoder initialization: {0}")]
    UnknownError(i32),
}

impl From<i32> for OpenDecoderError {
    fn from(err_code: i32) -> Self {
        match err_code {
            AVERROR_OUT_OF_MEMORY => OpenDecoderError::OutOfMemory,
            AVERROR_INVALID_ARGUMENT => OpenDecoderError::InvalidArgument,
            AVERROR_NOT_IMPLEMENTED => OpenDecoderError::NotImplemented,
            AVERROR_AGAIN => OpenDecoderError::TryAgain,
            AVERROR_IO_ERROR => OpenDecoderError::IOError,
            _ => OpenDecoderError::UnknownError(err_code),
        }
    }
}

/// FFmpeg-level errors during decoding, carried by
/// [`DecodingOperationError`].
#[derive(thiserror::Error, Debug)]
#[non_exhaustive]
pub enum DecodingError {
    /// Memory allocation failed (`AVERROR(ENOMEM)`).
    #[error("Memory allocation error")]
    OutOfMemory,

    /// FFmpeg rejected an argument as invalid (`AVERROR(EINVAL)`).
    #[error("Invalid argument provided")]
    InvalidArgument,

    /// A low-level I/O error occurred (`AVERROR(EIO)`).
    #[error("I/O error occurred during decoding")]
    IOError,

    /// The operation timed out (`AVERROR(ETIMEDOUT)`).
    #[error("Timeout occurred during decoding")]
    Timeout,

    /// The stream or data connection was broken (`AVERROR(EPIPE)`).
    #[error("Broken pipe encountered during decoding")]
    PipeError,

    /// An invalid file descriptor was used (`AVERROR(EBADF)`).
    #[error("Bad file descriptor encountered during decoding")]
    BadFileDescriptor,

    /// The functionality or format is not supported by the linked FFmpeg
    /// build (`AVERROR(ENOSYS)`).
    #[error("Unsupported functionality or format encountered")]
    NotImplemented,

    /// The operation was not permitted (`AVERROR(EPERM)`).
    #[error("Operation not permitted")]
    OperationNotPermitted,

    /// The decoder is temporarily unable to accept or produce data
    /// (`AVERROR(EAGAIN)`).
    #[error("Resource temporarily unavailable")]
    TryAgain,

    /// Any other failure; the payload is the raw FFmpeg error code.
    #[error("An unknown decoding error occurred. ret:{0}")]
    UnknownError(i32),
}

impl From<i32> for DecodingError {
    fn from(err_code: i32) -> Self {
        match err_code {
            AVERROR_OUT_OF_MEMORY => DecodingError::OutOfMemory,
            AVERROR_INVALID_ARGUMENT => DecodingError::InvalidArgument,
            AVERROR_IO_ERROR => DecodingError::IOError,
            AVERROR_TIMEOUT => DecodingError::Timeout,
            AVERROR_PIPE_ERROR => DecodingError::PipeError,
            AVERROR_BAD_FILE_DESCRIPTOR => DecodingError::BadFileDescriptor,
            AVERROR_NOT_IMPLEMENTED => DecodingError::NotImplemented,
            AVERROR_OPERATION_NOT_PERMITTED => DecodingError::OperationNotPermitted,
            AVERROR_AGAIN => DecodingError::TryAgain,
            _ => DecodingError::UnknownError(err_code),
        }
    }
}

/// Errors from resolving a decoder.
#[derive(thiserror::Error, Debug)]
#[non_exhaustive]
pub enum DecoderError {
    /// Returned when a decoder requested by name is not provided by the
    /// linked FFmpeg build; the payload is the requested name.
    #[error("decoder '{0}' not found")]
    NotFound(String),
}

/// FFmpeg-level errors during demuxing, carried by
/// [`DemuxingOperationError`] and [`PacketScannerError`].
#[derive(thiserror::Error, Debug)]
#[non_exhaustive]
pub enum DemuxingError {
    /// Memory allocation failed (`AVERROR(ENOMEM)`).
    #[error("Memory allocation error")]
    OutOfMemory,

    /// FFmpeg rejected an argument as invalid (`AVERROR(EINVAL)`).
    #[error("Invalid argument provided")]
    InvalidArgument,

    /// A low-level I/O error occurred (`AVERROR(EIO)`).
    #[error("I/O error occurred during demuxing")]
    IOError,

    /// End of file was reached during demuxing (`AVERROR_EOF`).
    #[error("End of file reached during demuxing")]
    EndOfFile,

    /// The resource is temporarily unavailable (`AVERROR(EAGAIN)`).
    #[error("Resource temporarily unavailable")]
    TryAgain,

    /// The functionality is not supported by the linked FFmpeg build
    /// (`AVERROR(ENOSYS)`).
    #[error("Functionality not implemented or unsupported")]
    NotImplemented,

    /// The operation was not permitted (`AVERROR(EPERM)`).
    #[error("Operation not permitted")]
    OperationNotPermitted,

    /// An invalid file descriptor was used (`AVERROR(EBADF)`).
    #[error("Bad file descriptor encountered")]
    BadFileDescriptor,

    /// The input contains invalid or corrupted data
    /// (`AVERROR_INVALIDDATA`).
    #[error("Invalid data found when processing input")]
    InvalidData,

    /// Any other failure; the payload is the raw FFmpeg error code
    /// (rendered with `av_err2str` in the message).
    #[error("{}. ret:{0}", crate::util::ffmpeg_utils::av_err2str(*.0))]
    UnknownError(i32),
}

impl From<i32> for DemuxingError {
    fn from(err_code: i32) -> Self {
        match err_code {
            AVERROR_OUT_OF_MEMORY => DemuxingError::OutOfMemory,
            AVERROR_INVALID_ARGUMENT => DemuxingError::InvalidArgument,
            AVERROR_IO_ERROR => DemuxingError::IOError,
            AVERROR_EOF => DemuxingError::EndOfFile,
            AVERROR_AGAIN => DemuxingError::TryAgain,
            AVERROR_NOT_IMPLEMENTED => DemuxingError::NotImplemented,
            AVERROR_OPERATION_NOT_PERMITTED => DemuxingError::OperationNotPermitted,
            AVERROR_BAD_FILE_DESCRIPTOR => DemuxingError::BadFileDescriptor,
            AVERROR_INVALIDDATA => DemuxingError::InvalidData,
            _ => DemuxingError::UnknownError(err_code),
        }
    }
}

/// Errors that can occur during packet scanning operations.
#[derive(thiserror::Error, Debug)]
#[non_exhaustive]
pub enum PacketScannerError {
    /// Failed to seek to the requested timestamp.
    #[error("while seeking: {0}")]
    SeekError(DemuxingError),

    /// Failed to read the next packet from the demuxer.
    #[error("while reading packet: {0}")]
    ReadError(DemuxingError),
}

#[cfg(test)]
mod tests {
    // Regression: FrameSourceThreadExited is payload-less, but the manual
    // PartialEq whitelist omitted it, so the variant compared unequal to
    // itself — breaking the impl's documented "structural equality for
    // payload-less variants" contract.
    #[test]
    fn frame_source_thread_exited_equals_itself() {
        use super::Error;
        assert_eq!(
            Error::FrameSourceThreadExited,
            Error::FrameSourceThreadExited
        );
        assert_ne!(Error::FrameSourceThreadExited, Error::NotStarted);
    }

    // Regression: FilterGraphParseError declares PermissionDenied and NotSocket,
    // but its From<i32> once omitted them, so an EACCES/ENOTSOCK filtergraph
    // error degraded to UnknownError and the two declared variants were
    // unreachable. Map the codes to the variants the enum already exposes.
    #[test]
    fn filter_graph_parse_error_maps_permission_and_socket_codes() {
        use super::{FilterGraphParseError, AVERROR_NOT_SOCKET, AVERROR_PERMISSION_DENIED};
        assert!(matches!(
            FilterGraphParseError::from(AVERROR_PERMISSION_DENIED),
            FilterGraphParseError::PermissionDenied
        ));
        assert!(matches!(
            FilterGraphParseError::from(AVERROR_NOT_SOCKET),
            FilterGraphParseError::NotSocket
        ));
    }

    // make_frame_writable's failure is typed like every other AVERROR-coded
    // failure in this file: common codes map to named variants, the rest keep
    // the raw code. Pin the mapping and the user-facing Display string.
    #[test]
    fn frame_writable_error_maps_codes_and_pins_display() {
        use super::{Error, FrameWritableError, AVERROR_INVALID_ARGUMENT, AVERROR_OUT_OF_MEMORY};
        assert!(matches!(
            FrameWritableError::from(AVERROR_OUT_OF_MEMORY),
            FrameWritableError::OutOfMemory
        ));
        assert!(matches!(
            FrameWritableError::from(AVERROR_INVALID_ARGUMENT),
            FrameWritableError::InvalidArgument
        ));
        assert!(matches!(
            FrameWritableError::from(-99),
            FrameWritableError::UnknownError(-99)
        ));
        let err = Error::from(FrameWritableError::from(AVERROR_OUT_OF_MEMORY));
        assert_eq!(
            err.to_string(),
            "Frame writable error: Memory allocation error while copying frame data"
        );
    }

    // The deprecated OpenGL filter's constructor failures are typed like the
    // wgpu successor's: they carry OpenGLFilterError and convert into
    // Error::OpenGLFilter. Pin the user-facing Display strings.
    #[cfg(feature = "opengl")]
    #[test]
    fn opengl_filter_error_pins_display() {
        use super::{Error, OpenGLFilterError};
        let err = Error::from(OpenGLFilterError::InvalidOption(
            "fragment shader must declare 'in vec2 TexCoord;'".to_string(),
        ));
        assert_eq!(
            err.to_string(),
            "OpenGL filter error: invalid OpenGL filter option: \
             fragment shader must declare 'in vec2 TexCoord;'"
        );
        let err = Error::from(OpenGLFilterError::ContextCreation(
            "Failed to create Surfman connection".to_string(),
        ));
        assert_eq!(
            err.to_string(),
            "OpenGL filter error: OpenGL context creation failed: \
             Failed to create Surfman connection"
        );
    }
}