From edd6bb4a608149eee80d16b561991a78aec153e2 Mon Sep 17 00:00:00 2001 From: "Aode (lion)" Date: Sat, 28 Aug 2021 17:15:14 -0500 Subject: [PATCH] Replace all bindings with calls to external binaries --- Cargo.lock | 377 +++------------------------------- Cargo.toml | 14 +- docker/dev/Dockerfile.amd64 | 14 +- docker/dev/Dockerfile.arm32v7 | 14 +- docker/dev/Dockerfile.arm64v8 | 12 +- src/error.rs | 27 ++- src/exiv2.rs | 127 ++++++++++-- src/ffmpeg.rs | 89 ++++---- src/magick.rs | 149 ++++++++++++-- src/main.rs | 100 ++------- src/processor.rs | 174 ++++------------ src/range.rs | 17 +- src/upload_manager.rs | 49 +---- src/validate.rs | 67 ++++++ src/validate/mod.rs | 218 -------------------- src/validate/transcode.rs | 314 ---------------------------- 16 files changed, 479 insertions(+), 1283 deletions(-) create mode 100644 src/validate.rs delete mode 100644 src/validate/mod.rs delete mode 100644 src/validate/transcode.rs diff --git a/Cargo.lock b/Cargo.lock index cfb30d3..ba0d9d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -261,9 +261,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "0.7.15" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" dependencies = [ "memchr", ] @@ -368,66 +368,12 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" -[[package]] -name = "bindgen" -version = "0.56.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2da379dbebc0b76ef63ca68d8fc6e71c0f13e59432e0987e508c1820e6ab5239" -dependencies = [ - "bitflags", - "cexpr 0.4.0", - "clang-sys", - "lazy_static", - "lazycell", - "peeking_take_while", - "proc-macro2", - "quote", - "regex", - "rustc-hash", - "shlex 0.1.1", -] - -[[package]] -name = "bindgen" -version = "0.59.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "453c49e5950bb0eb63bb3df640e31618846c89d5b7faa54040d76e98e0134375" -dependencies = [ - "bitflags", - "cexpr 0.5.0", - "clang-sys", - "clap", - "env_logger", - "lazy_static", - "lazycell", - "log", - "peeking_take_while", - "proc-macro2", - "quote", - "regex", - "rustc-hash", - "shlex 1.0.0", - "which", -] - [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" -[[package]] -name = "bitvec" -version = "0.19.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8942c8d352ae1838c9dda0b0ca2ab657696ef2232a20147cf1b30ae1a9cb4321" -dependencies = [ - "funty", - "radium", - "tap", - "wyz", -] - [[package]] name = "block-buffer" version = "0.9.0" @@ -464,30 +410,6 @@ dependencies = [ "bytes", ] -[[package]] -name = "cc" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e70cc2f62c6ce1868963827bd677764c62d07c3d9a3e1fb1177ee1a9ab199eb2" - -[[package]] -name = "cexpr" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4aedb84272dbe89af497cf81375129abda4fc0a9e7c5d317498c15cc30c0d27" -dependencies = [ - "nom 5.1.2", -] - -[[package]] -name = "cexpr" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db507a7679252d2276ed0dd8113c6875ec56d3089f9225b2b42c30cc1f8e5c89" -dependencies = [ - "nom 6.2.1", -] - [[package]] name = "cfg-if" version = "1.0.0" @@ -506,17 +428,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "clang-sys" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81cf2cc85830eae84823884db23c5306442a6c3d5bfd3beb2f2a2c829faa1816" -dependencies = [ - "glob", - "libc", - "libloading", -] - [[package]] name = "clap" version = "2.33.3" @@ -546,9 +457,9 @@ checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" [[package]] name = "cpufeatures" -version = "0.1.5" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66c99696f6c9dd7f35d486b9d04d7e6e202aa3e8c40d553f2fdf5e7e0c6a71ef" +checksum = "95059428f66df56b63431fdb4e1947ed2190586af5c5a8a8b71122bdf5a7f469" dependencies = [ "libc", ] @@ -628,44 +539,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "env_logger" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3" -dependencies = [ - "atty", - "humantime", - "log", - "regex", - "termcolor", -] - -[[package]] -name = "ffmpeg-next" -version = "4.4.0-dev" -source = "git+https://github.com/jwiesler/rust-ffmpeg#1966244b303a365d22eca0e33160e58b3d8ce903" -dependencies = [ - "bitflags", - "ffmpeg-sys-next", - "libc", - "log", - "vsprintf", -] - -[[package]] -name = "ffmpeg-sys-next" -version = "4.3.5" -source = "git+https://github.com/jwiesler/rust-ffmpeg-sys#5d819d172f116a8a6d960df35e4d5bc8b4a82924" -dependencies = [ - "bindgen 0.56.0", - "cc", - "libc", - "num_cpus", - "pkg-config", - "vcpkg", -] - [[package]] name = "fnv" version = "1.0.7" @@ -692,12 +565,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "funty" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7" - [[package]] name = "futures" version = "0.3.16" @@ -822,22 +689,6 @@ dependencies = [ "wasi", ] -[[package]] -name = "gexiv2-sys" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3e66ac4aab8b401ca63838116d080b98e12fb5056e1c2a44e35c93da3526d13" -dependencies = [ - "libc", - "pkg-config", -] - -[[package]] -name = "glob" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" - [[package]] name = "h2" version = "0.3.4" @@ -898,12 +749,6 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acd94fdbe1d4ff688b67b04eee2e17bd50995534a61539e45adfefb45e5e5503" -[[package]] -name = "humantime" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" - [[package]] name = "idna" version = "0.2.3" @@ -952,28 +797,12 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" -[[package]] -name = "lazycell" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" - [[package]] name = "libc" version = "0.2.101" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3cb00336871be5ed2c8ed44b60ae9959dc5b9f08539422ed43f09e34ecaeba21" -[[package]] -name = "libloading" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f84d96438c15fcd6c3f244c8fce01d1e2b9c6b5623e9c711dc9286d8fc92d6a" -dependencies = [ - "cfg-if", - "winapi", -] - [[package]] name = "local-channel" version = "0.1.2" @@ -994,9 +823,9 @@ checksum = "84f9a2d3e27ce99ce2c3aad0b09b1a7b916293ea9b2bf624c13fe646fadd8da4" [[package]] name = "lock_api" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0382880606dff6d15c9476c416d18690b72742aa7b605bb6dd6ec9030fbf07eb" +checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109" dependencies = [ "scopeguard", ] @@ -1010,17 +839,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "magick_rust" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13584c9d754ca069ee43d1ee155c4a14a87e7e68faf7b61255f2d3133ee259c4" -dependencies = [ - "bindgen 0.59.1", - "libc", - "pkg-config", -] - [[package]] name = "matchers" version = "0.0.1" @@ -1038,9 +856,9 @@ checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" [[package]] name = "memchr" -version = "2.3.4" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" +checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" [[package]] name = "memoffset" @@ -1079,28 +897,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "nom" -version = "5.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af" -dependencies = [ - "memchr", - "version_check", -] - -[[package]] -name = "nom" -version = "6.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c5c51b9083a3c620fa67a2a635d1ce7d95b897e957d6b28ff9a5da960a103a6" -dependencies = [ - "bitvec", - "funty", - "memchr", - "version_check", -] - [[package]] name = "ntapi" version = "0.3.6" @@ -1120,17 +916,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "num-rational" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c000134b5dbf44adc5cb772486d335293351644b801551abe8f75c84cfa4aef" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - [[package]] name = "num-traits" version = "0.2.14" @@ -1164,9 +949,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "parking_lot" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d7744ac029df22dca6284efe4e898991d28e3085c706c972bcd7da4a27a15eb" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" dependencies = [ "instant", "lock_api", @@ -1175,9 +960,9 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.8.3" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7a782938e745763fe6907fc6ba86946d72f49fe7e21de074e08128a99fb018" +checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" dependencies = [ "cfg-if", "instant", @@ -1193,12 +978,6 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf547ad0c65e31259204bd90935776d1c693cec2f4ff7abb7a1bbbd40dfe58" -[[package]] -name = "peeking_take_while" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" - [[package]] name = "percent-encoding" version = "2.1.0" @@ -1226,15 +1005,11 @@ dependencies = [ "async-stream", "awc", "base64", - "ffmpeg-next", - "ffmpeg-sys-next", "futures", - "magick_rust", "mime", "num_cpus", "once_cell", "rand", - "rexiv2", "serde", "serde_json", "sha2", @@ -1281,12 +1056,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" -[[package]] -name = "pkg-config" -version = "0.3.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" - [[package]] name = "ppv-lite86" version = "0.2.10" @@ -1347,12 +1116,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "radium" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8" - [[package]] name = "rand" version = "0.8.4" @@ -1404,9 +1167,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.4.6" +version = "1.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a26af418b574bd56588335b3a3659a65725d4e636eb1016c2f9e3b38c7cc759" +checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" dependencies = [ "aho-corasick", "memchr", @@ -1428,23 +1191,6 @@ version = "0.6.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" -[[package]] -name = "rexiv2" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab34307334dcf90c2f9afeda08e0c52310f4d9f282cf6e8ec6a6ffb0b09826a5" -dependencies = [ - "gexiv2-sys", - "libc", - "num-rational", -] - -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - [[package]] name = "rustc_version" version = "0.2.3" @@ -1510,18 +1256,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.129" +version = "1.0.130" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1f72836d2aa753853178eda473a3b9d8e4eefdaf20523b919677e6de489f8f1" +checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.129" +version = "1.0.130" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e57ae87ad533d9a56427558b516d0adac283614e347abf85b0dc0cbbf0a249f3" +checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b" dependencies = [ "proc-macro2", "quote", @@ -1530,9 +1276,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.66" +version = "1.0.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "336b10da19a12ad094b59d870ebde26a45402e5b470add4b5fd03c5048a32127" +checksum = "a7f9e390c27c3c0ce8bc5d725f6e4d30a29d26659494aa4b17535f7522c5c950" dependencies = [ "itoa", "ryu", @@ -1553,9 +1299,9 @@ dependencies = [ [[package]] name = "sha-1" -version = "0.9.7" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a0c8611594e2ab4ebbf06ec7cbbf0a99450b8570e96cbf5188b5d5f6ef18d81" +checksum = "99cd6713db3cf16b6c84e06321e049a9b9f699826e16096d23bbcc44d15d51a6" dependencies = [ "block-buffer", "cfg-if", @@ -1572,9 +1318,9 @@ checksum = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d" [[package]] name = "sha2" -version = "0.9.5" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b362ae5752fd2137731f9fa25fd4d9058af34666ca1966fb969119cc35719f12" +checksum = "9204c41a1597a8c5af23c82d1c921cb01ec0a4c59e07a9c7306062829a3903f3" dependencies = [ "block-buffer", "cfg-if", @@ -1592,18 +1338,6 @@ dependencies = [ "lazy_static", ] -[[package]] -name = "shlex" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fdf1b9db47230893d76faad238fd6097fd6d6a9245cd7a4d90dbd639536bbd2" - -[[package]] -name = "shlex" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42a568c8f2cd051a4d283bd6eb0343ac214c1b0f1ac19f93e1175b2dee38c73d" - [[package]] name = "signal-hook-registry" version = "1.4.0" @@ -1750,21 +1484,6 @@ dependencies = [ "unicode-xid", ] -[[package]] -name = "tap" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" - -[[package]] -name = "termcolor" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" -dependencies = [ - "winapi-util", -] - [[package]] name = "textwrap" version = "0.11.0" @@ -1776,18 +1495,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93119e4feac1cbe6c798c34d3a53ea0026b0b1de6a120deef895137c0529bfe2" +checksum = "e1c319f97498ee34e17e1d7813fcd28a0ec1aaf350a4c44883d2fe741edb1c70" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "060d69a0afe7796bf42e9e2ff91f5ee691fb15c53d38b4b62a9a53eb23164745" +checksum = "8bf955fbafde33573fd32e90312488fa2ea68f7a220a5faab1809fa90690224f" dependencies = [ "proc-macro2", "quote", @@ -2067,12 +1786,6 @@ dependencies = [ "getrandom", ] -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - [[package]] name = "vec_map" version = "0.8.2" @@ -2085,16 +1798,6 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" -[[package]] -name = "vsprintf" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aec2f81b75ca063294776b4f7e8da71d1d5ae81c2b1b149c8d89969230265d63" -dependencies = [ - "cc", - "libc", -] - [[package]] name = "wasi" version = "0.10.2+wasi-snapshot-preview1" @@ -2155,15 +1858,6 @@ version = "0.2.76" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acdb075a845574a1fa5f09fd77e43f7747599301ea3417a9fbffdeedfc1f4a29" -[[package]] -name = "which" -version = "3.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d011071ae14a2f6671d0b74080ae0cd8ebf3a6f8c9589a2cd45f23126fe29724" -dependencies = [ - "libc", -] - [[package]] name = "winapi" version = "0.3.9" @@ -2180,23 +1874,8 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" -[[package]] -name = "winapi-util" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" -dependencies = [ - "winapi", -] - [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "wyz" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" diff --git a/Cargo.toml b/Cargo.toml index 0d0b7e0..dbc3250 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,12 +20,10 @@ async-stream = "0.3.0" awc = { version = "3.0.0-beta.7", default-features = false } base64 = "0.13.0" futures = "0.3.4" -magick_rust = { version = "0.15.0" } mime = "0.3.1" num_cpus = "1" once_cell = "1.4.0" rand = "0.8.0" -rexiv2 = "0.9.1" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" sha2 = "0.9.0" @@ -33,18 +31,8 @@ sled = { version = "0.34.6" } structopt = "0.3.14" thiserror = "1.0" time = { version = "0.2.23", features = ["serde"] } -tokio = { version = "1", default-features = false, features = ["sync", "process"] } +tokio = { version = "1", default-features = false, features = ["io-util", "process", "sync"] } tracing = "0.1.15" tracing-futures = "0.2.4" tracing-subscriber = { version = "0.2.5", features = ["fmt", "tracing-log"] } uuid = { version = "0.8", features = ["v4"] } - -[dependencies.ffmpeg-next] -version = "4.4.0-dev" -default-features = false -features = ["codec", "filter", "device", "format", "resampling", "postprocessing", "software-resampling", "software-scaling"] -git = "https://github.com/jwiesler/rust-ffmpeg" - -[dependencies.ffmpeg-sys-next] -version = "4.3.5" -git = "https://github.com/jwiesler/rust-ffmpeg-sys" diff --git a/docker/dev/Dockerfile.amd64 b/docker/dev/Dockerfile.amd64 index bbbcdb8..60762dd 100644 --- a/docker/dev/Dockerfile.amd64 +++ b/docker/dev/Dockerfile.amd64 @@ -78,7 +78,7 @@ RUN \ --disable-static \ --disable-docs \ --prefix=/usr/local \ - --with-utilities=no \ + --with-utilities=yes \ --with-magick-plus-plus=no \ --without-perl \ --with-xml=yes \ @@ -121,7 +121,7 @@ FROM cross-build as pict-rs-builder RUN \ apt-get install -y \ - libgexiv2-dev:$ARCH \ + exiv2:$ARCH \ libxml2:$ARCH \ libltdl7:$ARCH \ llvm-dev \ @@ -133,15 +133,7 @@ RUN \ libwebpdemux2:$ARCH \ libwebpmux3:$ARCH \ libgomp1:$ARCH \ - libavcodec-dev:$ARCH \ - libavfilter-dev:$ARCH \ - libavdevice-dev:$ARCH \ - libavformat-dev:$ARCH \ - libavresample-dev:$ARCH \ - libavutil-dev:$ARCH \ - libswscale-dev:$ARCH \ - libswresample-dev:$ARCH \ - ffmpeg + ffmpeg:$ARCH ENV \ PATH=$PATH:/opt/build/.cargo/bin \ diff --git a/docker/dev/Dockerfile.arm32v7 b/docker/dev/Dockerfile.arm32v7 index 357eb4d..95118db 100644 --- a/docker/dev/Dockerfile.arm32v7 +++ b/docker/dev/Dockerfile.arm32v7 @@ -78,7 +78,7 @@ RUN \ --disable-static \ --disable-docs \ --prefix=/usr/local \ - --with-utilities=no \ + --with-utilities=yes \ --with-magick-plus-plus=no \ --without-perl \ --with-xml=yes \ @@ -121,7 +121,7 @@ FROM cross-build as pict-rs-builder RUN \ apt-get install -y \ - libgexiv2-dev:$ARCH \ + exiv2:$ARCH \ libxml2:$ARCH \ libltdl7:$ARCH \ llvm-dev \ @@ -133,15 +133,7 @@ RUN \ libwebpdemux2:$ARCH \ libwebpmux3:$ARCH \ libgomp1:$ARCH \ - libavcodec-dev:$ARCH \ - libavfilter-dev:$ARCH \ - libavdevice-dev:$ARCH \ - libavformat-dev:$ARCH \ - libavresample-dev:$ARCH \ - libavutil-dev:$ARCH \ - libswscale-dev:$ARCH \ - libswresample-dev:$ARCH \ - ffmpeg && \ + ffmpeg:$ARCH && \ rm -rf /usr/include/x86_64-linux-gnu ENV \ diff --git a/docker/dev/Dockerfile.arm64v8 b/docker/dev/Dockerfile.arm64v8 index 5e4bbe8..c336a1e 100644 --- a/docker/dev/Dockerfile.arm64v8 +++ b/docker/dev/Dockerfile.arm64v8 @@ -78,7 +78,7 @@ RUN \ --disable-static \ --disable-docs \ --prefix=/usr/local \ - --with-utilities=no \ + --with-utilities=yes \ --with-magick-plus-plus=no \ --without-perl \ --with-xml=yes \ @@ -121,7 +121,7 @@ FROM cross-build as pict-rs-builder RUN \ apt-get install -y \ - libgexiv2-dev:$ARCH \ + exiv2:$ARCH \ libxml2:$ARCH \ libltdl7:$ARCH \ llvm-dev \ @@ -133,14 +133,6 @@ RUN \ libwebpdemux2:$ARCH \ libwebpmux3:$ARCH \ libgomp1:$ARCH \ - libavcodec-dev:$ARCH \ - libavfilter-dev:$ARCH \ - libavdevice-dev:$ARCH \ - libavformat-dev:$ARCH \ - libavresample-dev:$ARCH \ - libavutil-dev:$ARCH \ - libswscale-dev:$ARCH \ - libswresample-dev:$ARCH \ ffmpeg && \ rm -rf /usr/include/x86_64-linux-gnu diff --git a/src/error.rs b/src/error.rs index 515541b..6bcab68 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,4 +1,4 @@ -use crate::validate::GifError; +use crate::{exiv2::Exvi2Error, ffmpeg::VideoError, magick::MagickError}; use actix_web::{http::StatusCode, HttpResponse, ResponseError}; #[derive(Debug, thiserror::Error)] @@ -57,26 +57,23 @@ pub(crate) enum UploadError { #[error("Tried to save an image with an already-taken name")] DuplicateAlias, - #[error("Error validating Gif file, {0}")] - Gif(#[from] GifError), - - #[error("Error transcoding, {0}")] - Transcode(crate::validate::transcode::Error), - #[error("Tried to create file, but file already exists")] FileExists, - #[error("File metadata could not be parsed, {0}")] - Validate(#[from] rexiv2::Rexiv2Error), - - #[error("Error in MagickWand, {0}")] - Wand(String), - #[error("{0}")] Json(#[from] serde_json::Error), #[error("Range header not satisfiable")] Range, + + #[error("{0}")] + VideoError(#[from] VideoError), + + #[error("{0}")] + Exvi2Error(#[from] Exvi2Error), + + #[error("{0}")] + MagickError(#[from] MagickError), } impl From for UploadError { @@ -109,7 +106,9 @@ impl From for UploadError { impl ResponseError for UploadError { fn status_code(&self) -> StatusCode { match self { - UploadError::Gif(_) + UploadError::VideoError(_) + | UploadError::Exvi2Error(_) + | UploadError::MagickError(_) | UploadError::DuplicateAlias | UploadError::NoFiles | UploadError::Upload(_) diff --git a/src/exiv2.rs b/src/exiv2.rs index 4e80c04..8ee207e 100644 --- a/src/exiv2.rs +++ b/src/exiv2.rs @@ -1,10 +1,10 @@ #[derive(Debug, thiserror::Error)] -pub(crate) enum FormatError { +pub(crate) enum Exvi2Error { #[error("Failed to interface with exiv2")] IO(#[from] std::io::Error), - #[error("Failed to identify file")] - Status, + #[error("Mime Parse: {0}")] + Mime(#[from] mime::FromStrError), #[error("Identify semaphore is closed")] Closed, @@ -12,6 +12,9 @@ pub(crate) enum FormatError { #[error("Requested information is not present")] Missing, + #[error("Exiv2 command failed")] + Status, + #[error("Requested information was present, but not supported")] Unsupported, } @@ -21,6 +24,13 @@ pub(crate) enum ValidInputType { Gif, Png, Jpeg, + Webp, +} + +pub(crate) struct Details { + pub(crate) mime_type: mime::Mime, + pub(crate) width: usize, + pub(crate) height: usize, } static MAX_READS: once_cell::sync::OnceCell = @@ -30,48 +40,125 @@ fn semaphore() -> &'static tokio::sync::Semaphore { MAX_READS.get_or_init(|| tokio::sync::Semaphore::new(num_cpus::get() * 4)) } -pub(crate) async fn format

(file: P) -> Result +pub(crate) async fn clear_metadata

(file: P) -> Result<(), Exvi2Error> +where + P: AsRef, +{ + let permit = semaphore().acquire().await?; + + let status = tokio::process::Command::new("exiv2") + .arg(&"rm") + .arg(&file.as_ref()) + .spawn()? + .wait() + .await?; + + drop(permit); + + if !status.success() { + return Err(Exvi2Error::Status); + } + + Ok(()) +} + +pub(crate) async fn input_type

(file: P) -> Result where P: AsRef, { let permit = semaphore().acquire().await?; let output = tokio::process::Command::new("exiv2") - .args([ - &AsRef::::as_ref(&"pr"), - &file.as_ref().as_ref(), - ]) + .arg(&"pr") + .arg(&file.as_ref()) .output() .await?; drop(permit); - if !output.status.success() { - return Err(FormatError::Status); - } - let s = String::from_utf8_lossy(&output.stdout); - let line = s + let mime_line = s .lines() .find(|line| line.starts_with("MIME")) - .ok_or_else(|| FormatError::Missing)?; + .ok_or_else(|| Exvi2Error::Missing)?; - let mut segments = line.rsplit(':'); - let mime_type = segments.next().ok_or_else(|| FormatError::Missing)?; + let mut segments = mime_line.rsplit(':'); + let mime_type = segments.next().ok_or_else(|| Exvi2Error::Missing)?; let input_type = match mime_type.trim() { "video/mp4" => ValidInputType::Mp4, + "video/quicktime" => ValidInputType::Mp4, "image/gif" => ValidInputType::Gif, "image/png" => ValidInputType::Png, "image/jpeg" => ValidInputType::Jpeg, - _ => return Err(FormatError::Unsupported), + "image/webp" => ValidInputType::Webp, + _ => return Err(Exvi2Error::Unsupported), }; Ok(input_type) } -impl From for FormatError { - fn from(_: tokio::sync::AcquireError) -> FormatError { - FormatError::Closed +pub(crate) async fn details

(file: P) -> Result +where + P: AsRef, +{ + let permit = semaphore().acquire().await?; + + let output = tokio::process::Command::new("exiv2") + .arg(&"pr") + .arg(&file.as_ref()) + .output() + .await?; + drop(permit); + + let s = String::from_utf8_lossy(&output.stdout); + parse_output(s) +} + +fn parse_output(s: std::borrow::Cow<'_, str>) -> Result { + let mime_line = s + .lines() + .find(|line| line.starts_with("MIME")) + .ok_or_else(|| Exvi2Error::Missing)?; + + let mut segments = mime_line.rsplit(':'); + let mime_type = segments.next().ok_or_else(|| Exvi2Error::Missing)?.trim(); + + let resolution_line = s + .lines() + .find(|line| line.starts_with("Image size")) + .ok_or_else(|| Exvi2Error::Missing)?; + + let mut segments = resolution_line.rsplit(':'); + let resolution = segments.next().ok_or_else(|| Exvi2Error::Missing)?; + let mut resolution_segments = resolution.split('x'); + let width_str = resolution_segments + .next() + .ok_or_else(|| Exvi2Error::Missing)? + .trim(); + let height_str = resolution_segments + .next() + .ok_or_else(|| Exvi2Error::Missing)? + .trim(); + + let width = width_str.parse()?; + let height = height_str.parse()?; + + Ok(Details { + mime_type: mime_type.parse()?, + width, + height, + }) +} + +impl From for Exvi2Error { + fn from(_: tokio::sync::AcquireError) -> Exvi2Error { + Exvi2Error::Closed + } +} + +impl From for Exvi2Error { + fn from(_: std::num::ParseIntError) -> Exvi2Error { + Exvi2Error::Unsupported } } diff --git a/src/ffmpeg.rs b/src/ffmpeg.rs index 96d718c..d080c2b 100644 --- a/src/ffmpeg.rs +++ b/src/ffmpeg.rs @@ -10,6 +10,27 @@ pub(crate) enum VideoError { Closed, } +pub(crate) enum ThumbnailFormat { + Jpeg, + Webp, +} + +impl ThumbnailFormat { + fn as_codec(&self) -> &'static str { + match self { + ThumbnailFormat::Jpeg => "mjpeg", + ThumbnailFormat::Webp => "webp", + } + } + + fn as_format(&self) -> &'static str { + match self { + ThumbnailFormat::Jpeg => "singlejpeg", + ThumbnailFormat::Webp => "webp", + } + } +} + static MAX_TRANSCODES: once_cell::sync::OnceCell = once_cell::sync::OnceCell::new(); @@ -18,22 +39,6 @@ fn semaphore() -> &'static tokio::sync::Semaphore { .get_or_init(|| tokio::sync::Semaphore::new(num_cpus::get().saturating_sub(1).max(1))) } -pub(crate) async fn thumbnail_jpeg(from: P1, to: P2) -> Result<(), VideoError> -where - P1: AsRef, - P2: AsRef, -{ - thumbnail(from, to, "mjpeg").await -} - -pub(crate) async fn thumbnail_png(from: P1, to: P2) -> Result<(), VideoError> -where - P1: AsRef, - P2: AsRef, -{ - thumbnail(from, to, "png").await -} - pub(crate) async fn to_mp4(from: P1, to: P2) -> Result<(), VideoError> where P1: AsRef, @@ -42,20 +47,22 @@ where let permit = semaphore().acquire().await?; let mut child = tokio::process::Command::new("ffmpeg") + .arg(&"-i") + .arg(&from.as_ref()) .args([ - &AsRef::::as_ref(&"-i"), - &from.as_ref().as_ref(), - &"-movflags".as_ref(), - &"faststart".as_ref(), - &"-pix_fmt".as_ref(), - &"yuv420p".as_ref(), - &"-vf".as_ref(), - &"scale=trunc(iw/2)*2:truc(ih/2)*2".as_ref(), - &"-an".as_ref(), - &"-codec".as_ref(), - &"h264".as_ref(), - &to.as_ref().as_ref(), + &"-movflags", + &"faststart", + &"-pix_fmt", + &"yuv420p", + &"-vf", + &"scale=trunc(iw/2)*2:trunc(ih/2)*2", + &"-an", + &"-codec", + &"h264", + &"-f", + &"mp4", ]) + .arg(&to.as_ref()) .spawn()?; let status = child.wait().await?; @@ -68,7 +75,11 @@ where Ok(()) } -async fn thumbnail(from: P1, to: P2, codec: &str) -> Result<(), VideoError> +pub(crate) async fn thumbnail( + from: P1, + to: P2, + format: ThumbnailFormat, +) -> Result<(), VideoError> where P1: AsRef, P2: AsRef, @@ -76,17 +87,19 @@ where let permit = semaphore().acquire().await?; let mut child = tokio::process::Command::new("ffmpeg") + .arg(&"-i") + .arg(&from.as_ref()) .args([ - &AsRef::::as_ref(&"-i"), - &from.as_ref().as_ref(), - &"-ss".as_ref(), - &"00:00:01.000".as_ref(), - &"-vframes".as_ref(), - &"1".as_ref(), - &"-codec".as_ref(), - &codec.as_ref(), - &to.as_ref().as_ref(), + &"-ss", + &"00:00:01.000", + &"-vframes", + &"1", + &"-codec", + &format.as_codec(), + &"-f", + &format.as_format(), ]) + .arg(&to.as_ref()) .spawn()?; let status = child.wait().await?; diff --git a/src/magick.rs b/src/magick.rs index 53db6a5..4098107 100644 --- a/src/magick.rs +++ b/src/magick.rs @@ -1,28 +1,135 @@ -fn thumbnail_args(max_dimension: usize) -> [String; 2] { - [ - "-sample".to_string(), - format!("{}x{}>", max_dimension, max_dimension), - ] +#[derive(Debug, thiserror::Error)] +pub(crate) enum MagickError { + #[error("{0}")] + IO(#[from] std::io::Error), + + #[error("Magick command failed")] + Status, + + #[error("Magick semaphore is closed")] + Closed, + + #[error("Invalid format")] + Format, } -fn resize_args(max_dimension: usize) -> [String; 4] { - [ - "-filter".to_string(), - "Lanczos".to_string(), - "-resize".to_string(), - format!("{}x{}>", max_dimension, max_dimension), - ] +pub(crate) enum ValidFormat { + Jpeg, + Png, + Webp, } -fn crop_args(width: usize, height: usize) -> [String; 4] { - [ - "-gravity".to_string(), - "center".to_string(), - "-crop".to_string(), - format!("{}x{}>", width, height), - ] +impl ValidFormat { + fn as_magic_type(&self) -> &'static str { + match self { + ValidFormat::Jpeg => "JPEG", + ValidFormat::Png => "PNG", + ValidFormat::Webp => "WEBP", + } + } } -fn blur_args(radius: f64) -> [String; 2] { - ["-gaussian-blur".to_string(), radius.to_string()] +static MAX_CONVERSIONS: once_cell::sync::OnceCell = + once_cell::sync::OnceCell::new(); + +fn semaphore() -> &'static tokio::sync::Semaphore { + MAX_CONVERSIONS + .get_or_init(|| tokio::sync::Semaphore::new(num_cpus::get().saturating_sub(1).max(1))) +} + +pub(crate) async fn convert_file( + from: P1, + to: P2, + format: crate::config::Format, +) -> Result<(), MagickError> +where + P1: AsRef, + P2: AsRef, +{ + let mut output_file = std::ffi::OsString::new(); + output_file.extend([format.to_magick_format().as_ref(), ":".as_ref()]); + output_file.extend([to.as_ref().as_ref()]); + + let permit = semaphore().acquire().await?; + + let status = tokio::process::Command::new("magick") + .arg("convert") + .arg(&from.as_ref()) + .arg(&output_file) + .spawn()? + .wait() + .await?; + + drop(permit); + + if !status.success() { + return Err(MagickError::Status); + } + + Ok(()) +} + +pub(crate) async fn validate_format

(file: &P, format: ValidFormat) -> Result<(), MagickError> +where + P: AsRef, +{ + let permit = semaphore().acquire().await?; + + let output = tokio::process::Command::new("magick") + .args([&"identify", &"-ping", &"-format", &"%m\n"]) + .arg(&file.as_ref()) + .output() + .await?; + + drop(permit); + + let s = String::from_utf8_lossy(&output.stdout); + + if s.lines() + .all(|item| item.is_empty() || item == format.as_magic_type()) + { + return Ok(()); + } + + Err(MagickError::Format) +} + +pub(crate) async fn process_image( + input: P1, + output: P2, + args: Vec, + format: crate::config::Format, +) -> Result<(), MagickError> +where + P1: AsRef, + P2: AsRef, +{ + let mut output_file = std::ffi::OsString::new(); + output_file.extend([format!("{}:", format.to_magick_format()).as_ref()]); + output_file.extend([output.as_ref().as_ref()]); + + let permit = semaphore().acquire().await?; + + let status = tokio::process::Command::new("magick") + .arg(&"convert") + .arg(&input.as_ref()) + .args(args) + .arg(&output.as_ref()) + .spawn()? + .wait() + .await?; + + drop(permit); + + if !status.success() { + return Err(MagickError::Status); + } + + Ok(()) +} + +impl From for MagickError { + fn from(_: tokio::sync::AcquireError) -> MagickError { + MagickError::Closed + } } diff --git a/src/main.rs b/src/main.rs index 55639a6..ff026e8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,11 +6,9 @@ use actix_web::{ web, App, HttpResponse, HttpResponseBuilder, HttpServer, }; use awc::Client; -use futures::stream::{once, Stream, TryStreamExt}; +use futures::stream::{Stream, TryStreamExt}; use once_cell::sync::Lazy; -use std::{ - collections::HashSet, future::ready, path::PathBuf, pin::Pin, sync::Once, time::SystemTime, -}; +use std::{collections::HashSet, path::PathBuf, pin::Pin, time::SystemTime}; use structopt::StructOpt; use tracing::{debug, error, info, instrument, Span}; use tracing_subscriber::EnvFilter; @@ -19,6 +17,7 @@ mod config; mod error; mod exiv2; mod ffmpeg; +mod magick; mod middleware; mod migrate; mod processor; @@ -30,7 +29,6 @@ use self::{ config::{Config, Format}, error::UploadError, middleware::{Internal, Tracing}, - processor::process_image, upload_manager::{Details, UploadManager}, validate::{image_webp, video_mp4}, }; @@ -58,7 +56,6 @@ static TMP_DIR: Lazy = Lazy::new(|| { path }); static CONFIG: Lazy = Lazy::new(Config::from_args); -static MAGICK_INIT: Once = Once::new(); // try moving a file #[instrument] @@ -83,8 +80,11 @@ async fn safe_move_file(from: PathBuf, to: PathBuf) -> Result<(), UploadError> { Ok(()) } -async fn safe_create_parent(path: PathBuf) -> Result<(), UploadError> { - if let Some(path) = path.parent() { +async fn safe_create_parent

(path: P) -> Result<(), UploadError> +where + P: AsRef, +{ + if let Some(path) = path.as_ref().parent() { debug!("Creating directory {:?}", path); actix_fs::create_dir_all(path.to_owned()).await?; } @@ -285,7 +285,7 @@ async fn prepare_process( ext: &str, manager: &UploadManager, whitelist: &Option>, -) -> Result<(processor::ProcessChain, Format, String, PathBuf), UploadError> { +) -> Result<(Format, String, PathBuf, Vec), UploadError> { let (alias, operations) = query .into_inner() @@ -322,8 +322,9 @@ async fn prepare_process( let processed_name = format!("{}.{}", name, ext); let base = manager.image_dir(); let thumbnail_path = self::processor::build_path(base, &chain, processed_name); + let thumbnail_args = self::processor::build_args(&chain); - Ok((chain, format, name, thumbnail_path)) + Ok((format, name, thumbnail_path, thumbnail_args)) } async fn process_details( @@ -332,7 +333,7 @@ async fn process_details( manager: web::Data, whitelist: web::Data>>, ) -> Result { - let (_, _, name, thumbnail_path) = + let (_, name, thumbnail_path, _) = prepare_process(query, ext.as_str(), &manager, &whitelist).await?; let details = manager.variant_details(thumbnail_path, name).await?; @@ -351,7 +352,7 @@ async fn process( manager: web::Data, whitelist: web::Data>>, ) -> Result { - let (chain, format, name, thumbnail_path) = + let (format, name, thumbnail_path, thumbnail_args) = prepare_process(query, ext.as_str(), &manager, &whitelist).await?; // If the thumbnail doesn't exist, we need to create it @@ -396,78 +397,25 @@ async fn process( } } + safe_create_parent(&thumbnail_path).await?; + // apply chain to the provided image - let img_bytes = process_image(original_path.clone(), chain, format).await?; + magick::process_image(&original_path, &thumbnail_path, thumbnail_args, format).await?; - let path2 = thumbnail_path.clone(); - let img_bytes2 = img_bytes.clone(); - - let store_details = details.is_none(); let details = if let Some(details) = details { details } else { - let details = Details::from_bytes(&img_bytes)?; + let details = Details::from_path(&thumbnail_path).await?; manager - .store_variant_details(path2.clone(), name.clone(), &details) + .store_variant_details(thumbnail_path.clone(), name.clone(), &details) + .await?; + manager + .store_variant(thumbnail_path.clone(), name.clone()) .await?; details }; - // Save the file in another task, we want to return the thumbnail now - debug!("Spawning storage task"); - let span = Span::current(); - let details2 = details.clone(); - actix_rt::spawn(async move { - let entered = span.enter(); - if store_details { - debug!("Storing details"); - if let Err(e) = manager - .store_variant_details(path2.clone(), name.clone(), &details2) - .await - { - error!("Error storing details, {}", e); - return; - } - } - if let Err(e) = manager.store_variant(path2.clone(), name).await { - error!("Error storing variant, {}", e); - return; - } - - if let Err(e) = safe_save_file(path2, img_bytes2).await { - error!("Error saving file, {}", e); - } - drop(entered); - }); - - let (builder, stream) = match range { - Some(range_header) => { - if !range_header.is_bytes() { - return Err(UploadError::Range); - } - - if range_header.is_empty() { - return Err(UploadError::Range); - } else if range_header.len() == 1 { - let range = range_header.ranges().next().unwrap(); - - let mut builder = HttpResponse::PartialContent(); - builder.insert_header(range.to_content_range(img_bytes.len() as u64)); - (builder, range.chop_bytes(img_bytes)) - } else { - return Err(UploadError::Range); - } - } - None => (HttpResponse::Ok(), once(ready(Ok(img_bytes)))), - }; - - return Ok(srv_response( - builder, - stream, - details.content_type(), - 7 * DAYS, - details.system_time(), - )); + return ranged_file_resp(thumbnail_path, range, details).await; } let details = if let Some(details) = details { @@ -670,10 +618,6 @@ async fn filename_by_alias( #[actix_rt::main] async fn main() -> Result<(), anyhow::Error> { - MAGICK_INIT.call_once(|| { - magick_rust::magick_wand_genesis(); - }); - if std::env::var("RUST_LOG").is_err() { std::env::set_var("RUST_LOG", "info"); } diff --git a/src/processor.rs b/src/processor.rs index b1eaad8..eba9d80 100644 --- a/src/processor.rs +++ b/src/processor.rs @@ -1,12 +1,10 @@ -use crate::{ - config::Format, - error::UploadError, - validate::{ptos, Op}, -}; -use actix_web::web; -use magick_rust::MagickWand; +use crate::{error::UploadError, ffmpeg::ThumbnailFormat}; use std::path::PathBuf; -use tracing::{debug, error, instrument, Span}; +use tracing::{debug, error, instrument}; + +fn ptos(path: &PathBuf) -> Result { + Ok(path.to_str().ok_or(UploadError::Path)?.to_owned()) +} pub(crate) trait Processor { fn name() -> &'static str @@ -22,7 +20,7 @@ pub(crate) trait Processor { Self: Sized; fn path(&self, path: PathBuf) -> PathBuf; - fn process(&self, wand: &mut MagickWand) -> Result<(), UploadError>; + fn command(&self, args: Vec) -> Vec; } pub(crate) struct Identity; @@ -46,7 +44,6 @@ impl Processor for Identity { where Self: Sized, { - debug!("Identity"); Some(Box::new(Identity)) } @@ -54,8 +51,8 @@ impl Processor for Identity { path } - fn process(&self, _: &mut MagickWand) -> Result<(), UploadError> { - Ok(()) + fn command(&self, args: Vec) -> Vec { + args } } @@ -90,25 +87,10 @@ impl Processor for Thumbnail { path } - fn process(&self, wand: &mut MagickWand) -> Result<(), UploadError> { - debug!("Thumbnail"); - let width = wand.get_image_width(); - let height = wand.get_image_height(); + fn command(&self, mut args: Vec) -> Vec { + args.extend(["-sample".to_string(), format!("{}x{}>", self.0, self.0)]); - if width > self.0 || height > self.0 { - let width_ratio = width as f64 / self.0 as f64; - let height_ratio = height as f64 / self.0 as f64; - - let (new_width, new_height) = if width_ratio < height_ratio { - (width as f64 / height_ratio, self.0 as f64) - } else { - (self.0 as f64, height as f64 / width_ratio) - }; - - wand.op(|w| w.sample_image(new_width as usize, new_height as usize))?; - } - - Ok(()) + args } } @@ -143,29 +125,15 @@ impl Processor for Resize { path } - fn process(&self, wand: &mut MagickWand) -> Result<(), UploadError> { - debug!("Resize"); - let width = wand.get_image_width(); - let height = wand.get_image_height(); + fn command(&self, mut args: Vec) -> Vec { + args.extend([ + "-filter".to_string(), + "-Lanczos2".to_string(), + "-resize".to_string(), + format!("{}x{}>", self.0, self.0), + ]); - if width > self.0 || height > self.0 { - let width_ratio = width as f64 / self.0 as f64; - let height_ratio = height as f64 / self.0 as f64; - - let (new_width, new_height) = if width_ratio < height_ratio { - (width as f64 / height_ratio, self.0 as f64) - } else { - (self.0 as f64, height as f64 / width_ratio) - }; - - wand.resize_image( - new_width as usize, - new_height as usize, - magick_rust::bindings::FilterType_Lanczos2Filter, - ); - } - - Ok(()) + args } } @@ -208,40 +176,15 @@ impl Processor for Crop { path } - fn process(&self, wand: &mut MagickWand) -> Result<(), UploadError> { - let width = wand.get_image_width(); - let height = wand.get_image_height(); + fn command(&self, mut args: Vec) -> Vec { + args.extend([ + "-gravity".to_string(), + "center".to_string(), + "-crop".to_string(), + format!("{}:{}+0+0", self.0, self.1), + ]); - // 16x9 becomes 16/9, which is bigger than 16/10. a bigger number means a wider image - // - // Crop ratios bigger than Image ratios mean cropping the image's height and leaving the - // width alone. - let img_ratio = width as f64 / height as f64; - let crop_ratio = self.0 as f64 / self.1 as f64; - - let final_width; - let final_height; - - let x_offset; - let y_offset; - - if crop_ratio > img_ratio { - final_height = (width as f64 / self.0 as f64 * self.1 as f64) as usize; - final_width = width; - - x_offset = 0; - y_offset = ((height - final_height) as f64 / 2.0) as isize; - } else { - final_height = height; - final_width = (height as f64 / self.1 as f64 * self.0 as f64) as usize; - - x_offset = ((width - final_width) as f64 / 2.0) as isize; - y_offset = 0; - } - - wand.op(|w| w.crop_image(final_width, final_height, x_offset, y_offset))?; - - Ok(()) + args } } @@ -270,13 +213,10 @@ impl Processor for Blur { path } - fn process(&self, wand: &mut MagickWand) -> Result<(), UploadError> { - debug!("Blur"); - if self.0 > 0.0 { - wand.op(|w| w.gaussian_blur_image(0.0, self.0))?; - } + fn command(&self, mut args: Vec) -> Vec { + args.extend(["-gaussian-blur".to_string(), self.0.to_string()]); - Ok(()) + args } } @@ -333,6 +273,13 @@ pub(crate) fn build_path(base: PathBuf, chain: &ProcessChain, filename: String) path } +pub(crate) fn build_args(chain: &ProcessChain) -> Vec { + chain + .inner + .iter() + .fold(Vec::new(), |acc, processor| processor.command(acc)) +} + fn is_motion(s: &str) -> bool { s.ends_with(".gif") || s.ends_with(".mp4") } @@ -363,23 +310,17 @@ pub(crate) async fn prepare_image( let orig_path = original_path_str.clone(); let tmpfile = crate::tmp_file(); - crate::safe_create_parent(tmpfile.clone()).await?; - let tmpfile2 = tmpfile.clone(); + crate::safe_create_parent(&tmpfile).await?; - let res = web::block(move || { - use crate::validate::transcode::{transcode, Target}; - - transcode(orig_path, tmpfile, Target::Jpeg).map_err(UploadError::Transcode) - }) - .await?; + let res = crate::ffmpeg::thumbnail(orig_path, &tmpfile, ThumbnailFormat::Jpeg).await; if let Err(e) = res { error!("transcode error: {:?}", e); - actix_fs::remove_file(tmpfile2).await?; + actix_fs::remove_file(tmpfile.clone()).await?; return Err(e.into()); } - return match crate::safe_move_file(tmpfile2, jpg_path.clone()).await { + return match crate::safe_move_file(tmpfile, jpg_path.clone()).await { Err(UploadError::FileExists) => Ok(Some((jpg_path, Exists::Exists))), Err(e) => Err(e), _ => Ok(Some((jpg_path, Exists::New))), @@ -388,34 +329,3 @@ pub(crate) async fn prepare_image( Ok(None) } - -#[instrument] -pub(crate) async fn process_image( - original_file: PathBuf, - chain: ProcessChain, - format: Format, -) -> Result { - let original_path_str = ptos(&original_file)?; - - let span = Span::current(); - let bytes = web::block(move || { - let entered = span.enter(); - - let mut wand = MagickWand::new(); - debug!("Reading image"); - wand.op(|w| w.read_image(&original_path_str))?; - - debug!("Processing image"); - for processor in chain.inner.into_iter() { - debug!("Step"); - processor.process(&mut wand)?; - } - - let vec = wand.op(|w| w.write_image_blob(format.to_magick_format()))?; - drop(entered); - Ok(web::Bytes::from(vec)) as Result - }) - .await??; - - Ok(bytes) -} diff --git a/src/range.rs b/src/range.rs index 2943b1a..5602c03 100644 --- a/src/range.rs +++ b/src/range.rs @@ -8,12 +8,9 @@ use actix_web::{ web::Bytes, FromRequest, HttpRequest, }; -use futures::stream::{once, Once, Stream, StreamExt, TryStreamExt}; +use futures::stream::{Stream, StreamExt, TryStreamExt}; use std::{fs, io}; -use std::{ - future::{ready, Ready}, - pin::Pin, -}; +use std::{future::ready, pin::Pin}; #[derive(Debug)] pub(crate) enum Range { @@ -46,16 +43,6 @@ impl Range { } } - pub(crate) fn chop_bytes(&self, bytes: Bytes) -> Once>> { - match self { - Range::RangeStart(start) => once(ready(Ok(bytes.slice(*start as usize..)))), - Range::SuffixLength(from_start) => once(ready(Ok(bytes.slice(..*from_start as usize)))), - Range::Segment(start, end) => { - once(ready(Ok(bytes.slice(*start as usize..*end as usize)))) - } - } - } - pub(crate) async fn chop_file( &self, file: fs::File, diff --git a/src/upload_manager.rs b/src/upload_manager.rs index e6c2851..5279af3 100644 --- a/src/upload_manager.rs +++ b/src/upload_manager.rs @@ -96,47 +96,18 @@ pub(crate) struct Details { created_at: time::OffsetDateTime, } -fn mime_from_media_type(media_type: rexiv2::MediaType) -> mime::Mime { - match media_type { - rexiv2::MediaType::Jpeg => mime::IMAGE_JPEG, - rexiv2::MediaType::Png => mime::IMAGE_PNG, - rexiv2::MediaType::Gif => mime::IMAGE_GIF, - rexiv2::MediaType::Other(s) if s == "image/webp" => s.parse::().unwrap(), - rexiv2::MediaType::Other(s) if s == "video/mp4" || s == "video/quicktime" => { - "video/mp4".parse::().unwrap() - } - _ => mime::APPLICATION_OCTET_STREAM, - } -} - impl Details { - pub(crate) fn from_bytes(bytes: &[u8]) -> Result { - let metadata = rexiv2::Metadata::new_from_buffer(bytes)?; - let mime_type = mime_from_media_type(metadata.get_media_type()?); - let width = metadata.get_pixel_width(); - let height = metadata.get_pixel_height(); - let details = Details::now(width as usize, height as usize, mime_type); - Ok(details) - } + pub(crate) async fn from_path

(path: P) -> Result + where + P: AsRef, + { + let details = crate::exiv2::details(&path).await?; - pub(crate) async fn from_path(path: PathBuf) -> Result { - let (mime_type, width, height) = web::block(move || { - rexiv2::Metadata::new_from_path(&path).and_then(|metadata| { - metadata - .get_media_type() - .map(mime_from_media_type) - .map(|mime_type| { - ( - mime_type, - metadata.get_pixel_width(), - metadata.get_pixel_height(), - ) - }) - }) - }) - .await??; - - Ok(Details::now(width as usize, height as usize, mime_type)) + Ok(Details::now( + details.width, + details.height, + details.mime_type, + )) } fn now(width: usize, height: usize, content_type: mime::Mime) -> Self { diff --git a/src/validate.rs b/src/validate.rs new file mode 100644 index 0000000..791929b --- /dev/null +++ b/src/validate.rs @@ -0,0 +1,67 @@ +use crate::{ + config::Format, error::UploadError, exiv2::ValidInputType, magick::ValidFormat, tmp_file, +}; + +pub(crate) fn image_webp() -> mime::Mime { + "image/webp".parse().unwrap() +} + +pub(crate) fn video_mp4() -> mime::Mime { + "video/mp4".parse().unwrap() +} + +// import & export image using the image crate +#[tracing::instrument] +pub(crate) async fn validate_image( + tmpfile: std::path::PathBuf, + prescribed_format: Option, +) -> Result { + let input_type = crate::exiv2::input_type(&tmpfile).await?; + + match (prescribed_format, input_type) { + (_, ValidInputType::Gif) | (_, ValidInputType::Mp4) => { + let newfile = tmp_file(); + crate::safe_create_parent(&newfile).await?; + crate::ffmpeg::to_mp4(&tmpfile, &newfile).await?; + actix_fs::rename(newfile, tmpfile).await?; + + Ok(video_mp4()) + } + (Some(Format::Jpeg), ValidInputType::Jpeg) | (None, ValidInputType::Jpeg) => { + tracing::debug!("Validating format"); + crate::magick::validate_format(&tmpfile, ValidFormat::Jpeg).await?; + tracing::debug!("Clearing metadata"); + crate::exiv2::clear_metadata(&tmpfile).await?; + tracing::debug!("Validated"); + + Ok(mime::IMAGE_JPEG) + } + (Some(Format::Png), ValidInputType::Png) | (None, ValidInputType::Png) => { + tracing::debug!("Validating format"); + crate::magick::validate_format(&tmpfile, ValidFormat::Png).await?; + tracing::debug!("Clearing metadata"); + crate::exiv2::clear_metadata(&tmpfile).await?; + tracing::debug!("Validated"); + + Ok(mime::IMAGE_PNG) + } + (Some(Format::Webp), ValidInputType::Webp) | (None, ValidInputType::Webp) => { + tracing::debug!("Validating format"); + crate::magick::validate_format(&tmpfile, ValidFormat::Webp).await?; + tracing::debug!("Clearing metadata"); + crate::exiv2::clear_metadata(&tmpfile).await?; + tracing::debug!("Validated"); + + Ok(image_webp()) + } + (Some(format), _) => { + let newfile = tmp_file(); + crate::safe_create_parent(&newfile).await?; + crate::magick::convert_file(&tmpfile, &newfile, format.clone()).await?; + + actix_fs::rename(newfile, tmpfile).await?; + + Ok(format.to_mime()) + } + } +} diff --git a/src/validate/mod.rs b/src/validate/mod.rs deleted file mode 100644 index 56f1bd0..0000000 --- a/src/validate/mod.rs +++ /dev/null @@ -1,218 +0,0 @@ -use crate::{config::Format, error::UploadError, tmp_file}; -use actix_web::web; -use magick_rust::MagickWand; -use rexiv2::{MediaType, Metadata}; -use std::path::PathBuf; -use tracing::{debug, error, instrument, warn, Span}; - -pub(crate) mod transcode; - -use self::transcode::{Error as TranscodeError, Target}; - -pub(crate) trait Op { - fn op(&self, f: F) -> Result - where - F: Fn(&Self) -> Result; - - fn op_mut(&mut self, f: F) -> Result - where - F: Fn(&mut Self) -> Result; -} - -impl Op for MagickWand { - fn op(&self, f: F) -> Result - where - F: Fn(&Self) -> Result, - { - match f(self) { - Ok(t) => Ok(t), - Err(e) => { - if let Ok(e) = self.get_exception() { - error!("WandError: {}", e.0); - Err(UploadError::Wand(e.0)) - } else { - Err(UploadError::Wand(e.to_owned())) - } - } - } - } - - fn op_mut(&mut self, f: F) -> Result - where - F: Fn(&mut Self) -> Result, - { - match f(self) { - Ok(t) => Ok(t), - Err(e) => { - if let Ok(e) = self.get_exception() { - error!("WandError: {}", e.0); - Err(UploadError::Wand(e.0)) - } else { - Err(UploadError::Wand(e.to_owned())) - } - } - } - } -} - -#[derive(Debug, thiserror::Error)] -pub(crate) enum GifError { - #[error("{0}")] - Decode(#[from] TranscodeError), - - #[error("{0}")] - Io(#[from] std::io::Error), -} - -pub(crate) fn image_webp() -> mime::Mime { - "image/webp".parse().unwrap() -} - -pub(crate) fn video_mp4() -> mime::Mime { - "video/mp4".parse().unwrap() -} - -pub(crate) fn ptos(p: &PathBuf) -> Result { - Ok(p.to_str().ok_or(UploadError::Path)?.to_owned()) -} - -fn validate_format(file: &str, format: &str) -> Result<(), UploadError> { - let wand = MagickWand::new(); - debug!("reading"); - wand.op(|w| w.read_image(file))?; - - if wand.op(|w| w.get_image_format())? != format { - return Err(UploadError::UnsupportedFormat); - } - - Ok(()) -} - -fn safe_create_parent(path: &PathBuf) -> Result<(), UploadError> { - if let Some(path) = path.parent() { - std::fs::create_dir_all(path)?; - } - Ok(()) -} - -// import & export image using the image crate -#[instrument] -pub(crate) async fn validate_image( - tmpfile: PathBuf, - prescribed_format: Option, -) -> Result { - let tmpfile_str = ptos(&tmpfile)?; - let span = Span::current(); - - let content_type = web::block(move || { - let entered = span.enter(); - - let meta = Metadata::new_from_path(&tmpfile)?; - - let content_type = match (prescribed_format, meta.get_media_type()?) { - (_, MediaType::Gif) => { - let newfile = tmp_file(); - safe_create_parent(&newfile)?; - validate_frames(&tmpfile, &newfile)?; - - video_mp4() - } - (Some(Format::Jpeg), MediaType::Jpeg) | (None, MediaType::Jpeg) => { - validate_format(&tmpfile_str, "JPEG")?; - - meta.clear(); - meta.save_to_file(&tmpfile)?; - - mime::IMAGE_JPEG - } - (Some(Format::Png), MediaType::Png) | (None, MediaType::Png) => { - validate_format(&tmpfile_str, "PNG")?; - - meta.clear(); - meta.save_to_file(&tmpfile)?; - - mime::IMAGE_PNG - } - (Some(Format::Webp), MediaType::Other(webp)) | (None, MediaType::Other(webp)) - if webp == "image/webp" => - { - let newfile = tmp_file(); - safe_create_parent(&newfile)?; - let newfile_str = ptos(&newfile)?; - // clean metadata by writing new webp, since exiv2 doesn't support webp yet - { - let wand = MagickWand::new(); - - debug!("reading"); - wand.op(|w| w.read_image(&tmpfile_str))?; - - if wand.op(|w| w.get_image_format())? != "WEBP" { - return Err(UploadError::UnsupportedFormat); - } - - if let Err(e) = wand.op(|w| w.write_image(&newfile_str)) { - std::fs::remove_file(&newfile_str)?; - return Err(e); - } - } - - std::fs::rename(&newfile, &tmpfile)?; - - image_webp() - } - (Some(format), _) => { - let newfile = tmp_file(); - safe_create_parent(&newfile)?; - let newfile_str = ptos(&newfile)?; - { - let mut wand = MagickWand::new(); - - debug!("reading: {}", tmpfile_str); - wand.op(|w| w.read_image(&tmpfile_str))?; - - wand.op_mut(|w| w.set_image_format(format.to_magick_format()))?; - - debug!("writing: {}", newfile_str); - if let Err(e) = wand.op(|w| w.write_image(&newfile_str)) { - std::fs::remove_file(&newfile_str)?; - return Err(e); - } - } - - std::fs::rename(&newfile, &tmpfile)?; - - format.to_mime() - } - (_, MediaType::Other(mp4)) if mp4 == "video/mp4" || mp4 == "video/quicktime" => { - let newfile = tmp_file(); - safe_create_parent(&newfile)?; - validate_frames(&tmpfile, &newfile)?; - - video_mp4() - } - (_, media_type) => { - warn!("Unsupported media type, {}", media_type); - return Err(UploadError::UnsupportedFormat); - } - }; - - drop(entered); - Ok(content_type) as Result - }) - .await??; - - Ok(content_type) -} - -#[instrument] -fn validate_frames(from: &PathBuf, to: &PathBuf) -> Result<(), GifError> { - debug!("Transmuting GIF"); - - if let Err(e) = self::transcode::transcode(from, to, Target::Mp4) { - std::fs::remove_file(to)?; - return Err(e.into()); - } - - std::fs::rename(to, from)?; - Ok(()) -} diff --git a/src/validate/transcode.rs b/src/validate/transcode.rs deleted file mode 100644 index 6d02ad5..0000000 --- a/src/validate/transcode.rs +++ /dev/null @@ -1,314 +0,0 @@ -use ffmpeg_next::{ - self, codec, filter, format, frame, media, - util::{format::pixel::Pixel, rational::Rational}, -}; -use std::path::Path; - -#[derive(Debug, thiserror::Error)] -pub(crate) enum Error { - #[error("No video stream present")] - MissingVideo, - - #[error("Input format is not supported")] - UnsupportedFormat, - - #[error("No frame-rate present in input video")] - FrameRate, - - #[error("Filter {0} should have been set up by now")] - MissingFilter(&'static str), - - #[error("{0}")] - Transcode(#[from] ffmpeg_next::Error), -} - -#[derive(Clone, Copy, Debug)] -pub(crate) enum Target { - Mp4, - Jpeg, - #[allow(dead_code)] - Png, -} - -impl Target { - fn name(&self) -> &'static str { - match self { - Target::Mp4 => "mp4", - Target::Jpeg => "image2", - Target::Png => "image2", - } - } - - fn codec(&self) -> codec::id::Id { - match self { - Target::Mp4 => codec::id::Id::H264, - Target::Jpeg => codec::id::Id::MJPEG, - Target::Png => codec::id::Id::PNG, - } - } - - fn frames(&self) -> Option { - match self { - Target::Mp4 => None, - Target::Jpeg => Some(1), - Target::Png => Some(1), - } - } -} - -fn pixel_value(pixel: Pixel) -> i32 { - let av_px_fmt: ffmpeg_sys_next::AVPixelFormat = pixel.into(); - unsafe { std::mem::transmute::<_, i32>(av_px_fmt) } -} - -fn to_even(num: u32) -> u32 { - if num % 2 == 0 { - num - } else { - num - 1 - } -} - -fn filter( - decoder: &codec::decoder::Video, - encoder: &codec::encoder::Video, -) -> Result { - let mut filter = filter::Graph::new(); - - let aspect = Rational::new( - to_even(decoder.width()) as i32, - to_even(decoder.height()) as i32, - ) - .reduce(); - - let av_px_fmt = pixel_value(decoder.format()); - - let args = format!( - "video_size={width}x{height}:pix_fmt={pix_fmt}:time_base={time_base_num}/{time_base_den}:pixel_aspect={pix_aspect_num}/{pix_aspect_den}", - width=to_even(decoder.width()), - height=to_even(decoder.height()), - pix_fmt=av_px_fmt, - time_base_num=decoder.time_base().numerator(), - time_base_den=decoder.time_base().denominator(), - pix_aspect_num=aspect.numerator(), - pix_aspect_den=aspect.denominator(), - ); - - let buffer = filter::find("buffer").ok_or(Error::MissingFilter("buffer"))?; - filter.add(&buffer, "in", &args)?; - - let buffersink = filter::find("buffersink").ok_or(Error::MissingFilter("buffersink"))?; - let mut out = filter.add(&buffersink, "out", "")?; - out.set_pixel_format(encoder.format()); - - filter.output("in", 0)?.input("out", 0)?.parse("null")?; - filter.validate()?; - - println!("{}", filter.dump()); - - if let Some(codec) = encoder.codec() { - if !codec - .capabilities() - .contains(codec::capabilities::Capabilities::VARIABLE_FRAME_SIZE) - { - filter - .get("out") - .ok_or(Error::MissingFilter("out"))? - .sink() - .set_frame_size(encoder.frame_size()); - } - } - - Ok(filter) -} - -struct Transcoder { - stream: usize, - filter: filter::Graph, - decoder: codec::decoder::Video, - encoder: codec::encoder::Video, -} - -impl Transcoder { - fn decode( - &mut self, - packet: &ffmpeg_next::Packet, - decoded: &mut frame::Video, - ) -> Result { - self.decoder.decode(packet, decoded) - } - - fn add_frame(&mut self, decoded: &frame::Video) -> Result<(), Error> { - self.filter - .get("in") - .ok_or(Error::MissingFilter("out"))? - .source() - .add(decoded)?; - Ok(()) - } - - fn encode( - &mut self, - decoded: &mut frame::Video, - encoded: &mut ffmpeg_next::Packet, - octx: &mut format::context::Output, - in_time_base: Rational, - out_time_base: Rational, - ) -> Result<(), Error> { - while let Ok(()) = self - .filter - .get("out") - .ok_or(Error::MissingFilter("out"))? - .sink() - .frame(decoded) - { - if let Ok(true) = self.encoder.encode(decoded, encoded) { - encoded.set_stream(0); - encoded.rescale_ts(in_time_base, out_time_base); - encoded.write_interleaved(octx)?; - } - } - - Ok(()) - } - - fn flush( - &mut self, - decoded: &mut frame::Video, - encoded: &mut ffmpeg_next::Packet, - octx: &mut format::context::Output, - in_time_base: Rational, - out_time_base: Rational, - ) -> Result<(), Error> { - self.filter - .get("in") - .ok_or(Error::MissingFilter("in"))? - .source() - .flush()?; - - self.encode(decoded, encoded, octx, in_time_base, out_time_base)?; - - while let Ok(true) = self.encoder.flush(encoded) { - encoded.set_stream(0); - encoded.rescale_ts(in_time_base, out_time_base); - encoded.write_interleaved(octx)?; - } - Ok(()) - } -} - -fn transcoder( - ictx: &mut format::context::Input, - octx: &mut format::context::Output, - target: Target, -) -> Result { - let input = ictx - .streams() - .best(media::Type::Video) - .ok_or(Error::MissingVideo)?; - let mut decoder = input.codec().decoder().video()?; - let codec_id = target.codec(); - let codec = ffmpeg_next::encoder::find(codec_id) - .ok_or(Error::UnsupportedFormat)? - .video()?; - let global = octx - .format() - .flags() - .contains(format::flag::Flags::GLOBAL_HEADER); - - decoder.set_parameters(input.parameters())?; - - let mut output = octx.add_stream(codec)?; - let mut encoder = output.codec().encoder().video()?; - - if global { - encoder.set_flags(codec::flag::Flags::GLOBAL_HEADER); - } - - encoder.set_format( - codec - .formats() - .ok_or(Error::UnsupportedFormat)? - .next() - .ok_or(Error::UnsupportedFormat)?, - ); - encoder.set_bit_rate(decoder.bit_rate()); - encoder.set_max_bit_rate(decoder.max_bit_rate()); - - encoder.set_width(to_even(decoder.width())); - encoder.set_height(to_even(decoder.height())); - encoder.set_bit_rate(decoder.bit_rate()); - encoder.set_max_bit_rate(decoder.max_bit_rate()); - encoder.set_time_base(decoder.frame_rate().ok_or(Error::FrameRate)?.invert()); - output.set_time_base(decoder.time_base()); - - let encoder = encoder.open_as(codec)?; - output.set_parameters(&encoder); - - let filter = filter(&decoder, &encoder)?; - - Ok(Transcoder { - stream: input.index(), - filter, - decoder, - encoder, - }) -} - -pub(crate) fn transcode(input: P, output: Q, target: Target) -> Result<(), Error> -where - P: AsRef, - Q: AsRef, -{ - let mut ictx = format::input(&input)?; - let mut octx = format::output_as(&output, target.name())?; - let mut transcoder = transcoder(&mut ictx, &mut octx, target)?; - - octx.write_header()?; - - let in_time_base = transcoder.decoder.time_base(); - let out_time_base = octx.stream(0).ok_or(Error::MissingVideo)?.time_base(); - - let mut decoded = frame::Video::empty(); - let mut encoded = ffmpeg_next::Packet::empty(); - let mut count = 0; - - for (stream, mut packet) in ictx.packets() { - if stream.index() == transcoder.stream { - packet.rescale_ts(stream.time_base(), in_time_base); - - if let Ok(true) = transcoder.decode(&packet, &mut decoded) { - let timestamp = decoded.timestamp(); - decoded.set_pts(timestamp); - - transcoder.add_frame(&decoded)?; - - transcoder.encode( - &mut decoded, - &mut encoded, - &mut octx, - in_time_base, - out_time_base, - )?; - - count += 1; - } - } - - if target.frames().map(|f| count >= f).unwrap_or(false) { - break; - } - } - - transcoder.flush( - &mut decoded, - &mut encoded, - &mut octx, - in_time_base, - out_time_base, - )?; - - octx.write_trailer()?; - - Ok(()) -}