├── .github ├── dependabot.yml ├── release.yml └── workflows │ └── ci.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── Cargo.toml ├── LICENSE-APACHE.md ├── LICENSE-MIT.md ├── LICENSE.md ├── README.md ├── assets ├── bear.mp4 └── cat.jpg ├── build.rs ├── data ├── ffmpeg-5_1 │ └── binding.rs ├── ffmpeg-6_1 │ └── binding.rs ├── ffmpeg-7_1 │ └── binding.rs ├── hw_detect.md └── pixel.txt ├── examples ├── audio_mix_sources.rs ├── audio_recorder.rs ├── audio_sample.rs ├── colorous_render.rs ├── decoding.rs ├── encoding.rs ├── misc │ ├── av_convert.rs │ ├── av_spliter.rs │ ├── avio.rs │ ├── avio_reading.rs │ ├── avio_writing.rs │ ├── image_dump.rs │ ├── metadata.rs │ ├── mod.rs │ ├── thumbnail.rs │ └── tutorial01.rs ├── mod.rs └── muxing.rs ├── fonts ├── Arial.ttf └── DejaVuSansMono.ttf ├── src ├── codec.rs ├── colors.rs ├── decode.rs ├── encode.rs ├── filter.rs ├── flags.rs ├── frame.rs ├── hwaccel.rs ├── imgutils.rs ├── init.rs ├── io.rs ├── lib.rs ├── location.rs ├── mux.rs ├── options.rs ├── pixel.rs ├── resize.rs ├── stream.rs ├── swctx.rs ├── time.rs └── utils.rs └── tests ├── decode_audio.rs ├── decode_video.rs ├── encode_audio.rs ├── encode_video.rs ├── encoder_audio_test.rs ├── extract_mvs.rs ├── remux.rs ├── string_test.rs ├── transcode.rs ├── transcode_aac.rs ├── vaapi_encode.rs └── vaapi_transcode.rs /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "github-actions" 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" 12 | labels: 13 | - "exclude" 14 | commit-message: 15 | prefix: "chore(github-actions)" 16 | 17 | - package-ecosystem: "cargo" 18 | directory: "/" 19 | schedule: 20 | #interval: "daily" 21 | #interval: "monthly" 22 | interval: "weekly" 23 | labels: 24 | - "dependencies" 25 | commit-message: 26 | prefix: "chore(cargo)" 27 | reviewers: 28 | - "phial3" 29 | assignees: 30 | - "phial3" 31 | target-branch: "rsmpeg" 32 | open-pull-requests-limit: 5 33 | groups: 34 | rust: 35 | patterns: 36 | - "*" 37 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes 2 | # .github/release.yml 3 | 4 | changelog: 5 | exclude: 6 | labels: 7 | - "exclude" 8 | categories: 9 | - title: Dependencies 10 | labels: 11 | - "dependencies" 12 | - title: Changes 13 | labels: ["*"] 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /debug 4 | /target 5 | /output 6 | /build 7 | **/*/target/ 8 | 9 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 10 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 11 | **/*/Cargo.lock 12 | Cargo.lock 13 | 14 | # These are backup files generated by rustfmt 15 | **/*.rs.bk 16 | 17 | .justfile 18 | .vscode/ 19 | .idea/ 20 | .DS_Store -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at `xiaoyu@dromara.org`. 63 | All complaints will be reviewed and investigated promptly and fairly. 64 | 65 | All community leaders are obligated to respect the privacy and security of the 66 | reporter of any incident. 67 | 68 | ## Enforcement Guidelines 69 | 70 | Community leaders will follow these Community Impact Guidelines in determining 71 | the consequences for any action they deem in violation of this Code of Conduct: 72 | 73 | ### 1. Correction 74 | 75 | **Community Impact**: Use of inappropriate language or other behavior deemed 76 | unprofessional or unwelcome in the community. 77 | 78 | **Consequence**: A private, written warning from community leaders, providing 79 | clarity around the nature of the violation and an explanation of why the 80 | behavior was inappropriate. A public apology may be requested. 81 | 82 | ### 2. Warning 83 | 84 | **Community Impact**: A violation through a single incident or series 85 | of actions. 86 | 87 | **Consequence**: A warning with consequences for continued behavior. No 88 | interaction with the people involved, including unsolicited interaction with 89 | those enforcing the Code of Conduct, for a specified period of time. This 90 | includes avoiding interactions in community spaces as well as external channels 91 | like social media. Violating these terms may lead to a temporary or 92 | permanent ban. 93 | 94 | ### 3. Temporary Ban 95 | 96 | **Community Impact**: A serious violation of community standards, including 97 | sustained inappropriate behavior. 98 | 99 | **Consequence**: A temporary ban from any sort of interaction or public 100 | communication with the community for a specified period of time. No public or 101 | private interaction with the people involved, including unsolicited interaction 102 | with those enforcing the Code of Conduct, is allowed during this period. 103 | Violating these terms may lead to a permanent ban. 104 | 105 | ### 4. Permanent Ban 106 | 107 | **Community Impact**: Demonstrating a pattern of violation of community 108 | standards, including sustained inappropriate behavior, harassment of an 109 | individual, or aggression toward or disparagement of classes of individuals. 110 | 111 | **Consequence**: A permanent ban from any sort of public interaction within 112 | the community. 113 | 114 | ## Attribution 115 | 116 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 117 | version 2.0, available at 118 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 119 | 120 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 121 | enforcement ladder](https://github.com/mozilla/diversity). 122 | 123 | [homepage]: https://www.contributor-covenant.org 124 | 125 | For answers to common questions about this code of conduct, see the FAQ at 126 | https://www.contributor-covenant.org/faq. Translations are available at 127 | https://www.contributor-covenant.org/translations. -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rsmedia" 3 | version = "0.1.0" 4 | edition = "2021" 5 | build = "build.rs" 6 | readme = "README.md" 7 | license = "MIT OR Apache-2.0" 8 | description = "High-level video toolkit based on ffmpeg." 9 | keywords = ["video", "ffmpeg", "encoding", "decoding", "muxing"] 10 | categories = ["multimedia", "multimedia::video", "multimedia::audio"] 11 | authors = ["phial3"] 12 | repository = "https://github.com/dromara/rsmedia.git" 13 | exclude = [".github/*", "assets/*", "data/*", "examples/*", "tests/*", "benches/*", "docs/*", ] 14 | 15 | [features] 16 | default = ["ndarray", "ffmpeg7", "link_system_ffmpeg"] 17 | ffmpeg6 = ["rsmpeg/ffmpeg6"] 18 | ffmpeg7 = ["rsmpeg/ffmpeg7"] 19 | ## unxi linking ffmpeg with pkg-config. 20 | link_system_ffmpeg = ["rsmpeg/link_system_ffmpeg", "pkg-config"] 21 | ## windows linking ffmpeg with vcpkg. 22 | link_vcpkg_ffmpeg = ["rsmpeg/link_vcpkg_ffmpeg", "vcpkg"] 23 | 24 | [dependencies] 25 | pix = "0.14" 26 | rgb = "0.8" 27 | yuv = {version = "0.8", features = ["rayon", "professional_mode"]} 28 | color = "0.3" 29 | colorous = "1.0" 30 | colorutils-rs = "0.7" 31 | palette = "0.7" 32 | image = "0.25" 33 | imageproc = "0.25" 34 | num-traits = "0.2" 35 | anyhow = "1.0" 36 | log = "0.4" 37 | tracing = "0.1" 38 | url = "2.5" 39 | rand = "0.9" 40 | rayon = "1.10" 41 | dashmap = "6.1" 42 | once_cell = "1.21" 43 | num_cpus = "1.16" 44 | ndarray = { version = "0.16", optional = true } 45 | rsmpeg = { git = "https://github.com/phial3/rsmpeg", branch = "light", default-features = false } 46 | 47 | [dev-dependencies] 48 | libc = "0.2" 49 | camino = "1.1" 50 | chrono = "0.4" 51 | ringbuf = "0.4" 52 | ctor = "0.4" 53 | tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter", "chrono"] } 54 | ab_glyph = "0.2" 55 | libblur = "0.18" 56 | pic-scale = "0.6" 57 | fast_image_resize = "=5.1" 58 | histogram_equalization = "0.2" 59 | ## security & async 60 | futures = "0.3" 61 | tokio = { version = "1", features = ["full"] } 62 | ## sound & audio 63 | cpal = "0.15" 64 | wavers = "1.5" 65 | rubato = "0.16" 66 | dasp = { version = "0.11", features = ["all"] } 67 | rodio = { version = "0.20", features = ["symphonia-all", "tracing"] } 68 | 69 | [build-dependencies] 70 | vcpkg = { version = "0.2", optional = true } 71 | pkg-config = { version = "0.3", optional = true } 72 | 73 | [package.metadata.docs.rs] 74 | all-features = true 75 | 76 | [[example]] 77 | name = "decoding" 78 | path = "examples/decoding.rs" 79 | required-features = ["ndarray"] 80 | 81 | [[example]] 82 | name = "encoding" 83 | path = "examples/encoding.rs" 84 | required-features = ["ndarray"] 85 | -------------------------------------------------------------------------------- /LICENSE-APACHE.md: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright © 2025 rsmedia. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /LICENSE-MIT.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright © 2025 rsmedia. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) rsmedia. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without modification, 4 | are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this 7 | list of conditions and the following disclaimer. 8 | 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | 3. Neither the name of the copyright holder nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 21 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 22 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 23 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 24 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 25 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 26 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /assets/bear.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dromara/rsmedia/184f21cccb4a3041a9e38a19a17de7275ae87d0b/assets/bear.mp4 -------------------------------------------------------------------------------- /assets/cat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dromara/rsmedia/184f21cccb4a3041a9e38a19a17de7275ae87d0b/assets/cat.jpg -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::path::{Path, PathBuf}; 3 | 4 | fn main() { 5 | let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap(); 6 | let target_arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap(); 7 | 8 | match target_os.as_str() { 9 | "macos" => configure_macos(&target_arch), 10 | "linux" => configure_linux(&target_arch), 11 | "windows" => configure_windows(&target_arch), 12 | _ => panic!("Unsupported operating system"), 13 | } 14 | } 15 | 16 | #[allow(dead_code)] 17 | static FFMPEG_LIBS: [&str; 8] = [ 18 | "libavutil", 19 | "libavcodec", 20 | "libavformat", 21 | "libavdevice", 22 | "libavfilter", 23 | "libswscale", 24 | "libswresample", 25 | "libpostproc", 26 | ]; 27 | 28 | fn configure_macos(_target_arch: &str) { 29 | println!("cargo:rustc-link-lib=dylib=c"); 30 | println!("cargo:rustc-link-lib=dylib=dl"); 31 | println!("cargo:rustc-link-lib=dylib=pthread"); 32 | 33 | println!("cargo:rustc-link-search=native=/usr/lib"); 34 | println!("cargo:rustc-link-search=native=/usr/local/lib"); 35 | 36 | // Support Homebrew 37 | if Path::new("/opt/homebrew/lib").exists() { 38 | // Apple Silicon (M1/M2...) 39 | println!("cargo:rustc-link-search=native=/opt/homebrew/lib"); 40 | } 41 | if Path::new("/usr/local/opt").exists() { 42 | // Intel 43 | println!("cargo:rustc-link-search=native=/usr/local/opt"); 44 | } 45 | } 46 | 47 | fn configure_linux(target_arch: &str) { 48 | println!("cargo:rustc-link-lib=dylib=c"); 49 | println!("cargo:rustc-link-lib=dylib=dl"); 50 | println!("cargo:rustc-link-lib=dylib=pthread"); 51 | 52 | // arch-specific paths prioritized 53 | let arch_specific_paths = match target_arch { 54 | "x86_64" => vec!["/lib/x86_64-linux-gnu", "/usr/lib/x86_64-linux-gnu"], 55 | "aarch64" => vec!["/lib/aarch64-linux-gnu", "/usr/lib/aarch64-linux-gnu"], 56 | _ => vec![], 57 | }; 58 | for path in arch_specific_paths { 59 | println!("cargo:rustc-link-search=native={}", path); 60 | } 61 | 62 | // 1. To link prebuilt libraries: 63 | // Dynamic linking with pre-built dylib: 64 | // Set `FFMPEG_DLL_PATH` to the path of dll or so files. (Windows: Put corresponding .lib file next to the .dll file.) 65 | // 66 | // Static linking with pre-built static lib: 67 | // Set `FFMPEG_LIBS_DIR` to the path of FFmpeg pre-built libs directory. 68 | // 69 | // 2. To generate bindings: 70 | // Compile-time binding generation(requires the Clang dylib): 71 | // Set `FFMPEG_INCLUDE_DIR` to the path of the header files for binding generation. 72 | // 73 | // Use your prebuilt binding: 74 | // Set `FFMPEG_BINDING_PATH` to the pre-built binding file. 75 | // The pre-built binding is usually copied from the OUT_DIR of the compile-time binding generation, 76 | // using it will prevent the need to regenerate the same binding file repeatedly. 77 | // 78 | // ffmpeg libs 79 | #[cfg(target_os = "linux")] 80 | for lib in FFMPEG_LIBS.iter() { 81 | // println!("cargo:rustc-link-lib={}", lib); 82 | match pkg_config::probe_library(lib) { 83 | Ok(lib_info) => { 84 | println!("Found library: {}", lib); 85 | for path in lib_info.link_paths.iter() { 86 | println!("cargo:rustc-link-search=native={}", path.display()); 87 | } 88 | } 89 | Err(e) => { 90 | panic!("Could not find {} via pkg-config: {:?}", lib, e); 91 | } 92 | } 93 | } 94 | 95 | // common 96 | println!("cargo:rustc-link-search=native=/usr/local/lib"); 97 | println!("cargo:rustc-link-search=native=/usr/lib"); 98 | println!("cargo:rustc-link-search=native=/lib"); 99 | } 100 | 101 | fn configure_windows(target_arch: &str) { 102 | // 获取 VCPKG_ROOT 并检查 triplet 103 | let vcpkg_root = PathBuf::from(env::var("VCPKG_ROOT").expect("VCPKG_ROOT not found")); 104 | let triplets = if target_arch == "x86_64" { 105 | vec![ 106 | "x64-windows", 107 | "x64-windows-release", 108 | "x64-windows-static", 109 | "x64-windows-static-md", 110 | ] 111 | } else if target_arch == "aarch64" { 112 | vec![ 113 | "arm64-windows", 114 | "arm64-windows-static", 115 | "arm64-windows-static-md", 116 | ] 117 | } else { 118 | panic!("Unsupported target architecture: {}", target_arch); 119 | }; 120 | 121 | // 查找可用的 triplet 122 | let mut found_triplet = None; 123 | for triplet in triplets.iter() { 124 | let lib_path = vcpkg_root.join("installed").join(triplet).join("lib"); 125 | if lib_path.exists() { 126 | println!("cargo:warning=Found triplet: {}", triplet); 127 | found_triplet = Some((triplet, lib_path)); 128 | break; 129 | } 130 | } 131 | 132 | let (triplet, lib_path) = found_triplet.expect("No valid vcpkg triplets found!"); 133 | 134 | // 添加 vcpkg 库路径 135 | println!("cargo:rustc-link-search=native={}", lib_path.display()); 136 | 137 | // 配置运行时 138 | if triplet.ends_with("-static") { 139 | println!("cargo:rustc-link-arg=/NODEFAULTLIB:msvcrt.lib"); 140 | println!("cargo:rustc-link-arg=/DEFAULTLIB:libcmt.lib"); 141 | } else { 142 | println!("cargo:rustc-link-arg=/NODEFAULTLIB:libcmt.lib"); 143 | println!("cargo:rustc-link-arg=/DEFAULTLIB:msvcrt.lib"); 144 | } 145 | 146 | #[cfg(target_os = "windows")] 147 | { 148 | let ffmpeg = vcpkg::find_package("ffmpeg") 149 | .expect("Failed to find ffmpeg libs by vcpkg, please ensure vcpkg is installed."); 150 | 151 | // 定义一个函数来处理库名称提取和链接 152 | let rustc_link_libs = |libs: &[PathBuf], lib_type: &str| { 153 | for lib in libs { 154 | // 库的基本名称,不包含路径、前缀(如 "lib")或扩展名(如 ".dll"、".lib") 155 | let lib_name = lib.file_stem().unwrap_or_default().to_string_lossy(); 156 | // 如果名称包含连字符(如 "avcodec-61"),只保留连字符前的部分 157 | let lib_base_name = if let Some(idx) = lib_name.find('-') { 158 | lib_name[..idx].to_string() 159 | } else { 160 | lib_name.to_string() 161 | }; 162 | println!("cargo:rustc-link-lib={}={}", lib_type, lib_base_name); 163 | } 164 | }; 165 | 166 | if ffmpeg.is_static { 167 | rustc_link_libs(&ffmpeg.found_libs, "static"); 168 | } else { 169 | rustc_link_libs(&ffmpeg.found_dlls, "dylib"); 170 | } 171 | 172 | for path in ffmpeg.link_paths { 173 | println!("cargo:rustc-link-search=native={}", path.display()); 174 | } 175 | } 176 | 177 | // Windows 系统库 178 | let system_libs = [ 179 | // 基础系统库 180 | "kernel32", 181 | "user32", 182 | "gdi32", 183 | "advapi32", 184 | "shell32", 185 | "ole32", 186 | "oleaut32", 187 | "uuid", 188 | "ws2_32", 189 | // COM 和 Media Foundation 190 | "mf", 191 | "mfplat", 192 | "mfplay", 193 | "mfreadwrite", 194 | "mfuuid", 195 | "strmiids", 196 | "dxva2", 197 | "evr", 198 | // Security APIs 199 | "secur32", 200 | "security", 201 | "crypt32", 202 | "bcrypt", 203 | "ncrypt", 204 | "credui", 205 | "schannel", 206 | // 其他必需库 207 | "shlwapi", 208 | "psapi", 209 | "vfw32", 210 | "comdlg32", 211 | "comctl32", 212 | "msacm32", 213 | "winmm", 214 | ]; 215 | 216 | for lib in system_libs.iter() { 217 | println!("cargo:rustc-link-lib={}", lib); 218 | } 219 | 220 | // 链接器选项 221 | let mut linker_flags = vec![ 222 | // 多媒体核心库 223 | "mf.lib", 224 | "mfuuid.lib", 225 | "mfplat.lib", 226 | "mfplay.lib", 227 | "mfreadwrite.lib", 228 | "strmiids.lib", 229 | // 视频硬件加速 230 | "dxgi.lib", 231 | "dxva2.lib", 232 | "d3d11.lib", 233 | // 音频处理 234 | "winmm.lib", 235 | "dsound.lib", 236 | "ksuser.lib", 237 | // Windows系统核心库 238 | "ole32.lib", 239 | "user32.lib", 240 | "gdi32.lib", 241 | "shell32.lib", 242 | "advapi32.lib", 243 | "kernel32.lib", 244 | // 安全相关 245 | "secur32.lib", 246 | "crypt32.lib", 247 | "bcrypt.lib", 248 | "ncrypt.lib", 249 | // 基础安全选项 250 | "/NXCOMPAT", // 启用数据执行保护 (DEP) 251 | "/DYNAMICBASE", // 启用 ASLR 252 | "/HIGHENTROPYVA", // 启用高熵 ASLR 253 | "/LARGEADDRESSAWARE", // 启用大内存地址支持 254 | // 优化选项 255 | "/OPT:REF", // 删除未引用的函数和数据 256 | "/OPT:ICF", // 合并重复的函数 257 | "/INCREMENTAL:NO", // 禁用增量链接 258 | // 调试和安全检查 259 | "/DEBUG", // 包含调试信息 260 | "/DEBUGTYPE:CV", // 使用 CodeView 格式的调试信息 261 | ]; 262 | 263 | // 架构特定的链接器选项 264 | if target_arch == "x86_64" { 265 | linker_flags.extend_from_slice(&[ 266 | "/GUARD:CF", // 启用控制流保护 267 | "/CETCOMPAT", // 启用 CET Shadow Stack (仅 x64) 268 | ]); 269 | } else if target_arch == "aarch64" { 270 | linker_flags.push("/GUARD:CF"); // ARM64 支持 CFG,但不支持 CET 271 | } 272 | for flag in linker_flags.iter() { 273 | println!("cargo:rustc-link-arg={}", flag); 274 | } 275 | 276 | // Windows SDK 和 Visual Studio 路径 277 | if let Ok(windows_sdk_dir) = env::var("WindowsSdkDir") { 278 | let sdk_version = env::var("WindowsSDKLibVersion").unwrap_or("10.0.22621.0".to_string()); 279 | let arch_path = if target_arch == "x86_64" { 280 | "x64" 281 | } else { 282 | "arm64" 283 | }; 284 | 285 | let sdk_lib_path = PathBuf::from(windows_sdk_dir.clone()) 286 | .join("Lib") 287 | .join(&sdk_version) 288 | .join("um") 289 | .join(arch_path); 290 | if !sdk_lib_path.exists() { 291 | panic!("Windows SDK path not found: {}", sdk_lib_path.display()); 292 | } 293 | println!("cargo:rustc-link-search=native={}", sdk_lib_path.display()); 294 | 295 | let sdk_ucrt_path = PathBuf::from(windows_sdk_dir) 296 | .join("Lib") 297 | .join(&sdk_version) 298 | .join("ucrt") 299 | .join(arch_path); 300 | if !sdk_ucrt_path.exists() { 301 | panic!( 302 | "Windows SDK UCRT path not found: {}", 303 | sdk_ucrt_path.display() 304 | ); 305 | } 306 | println!("cargo:rustc-link-search=native={}", sdk_ucrt_path.display()); 307 | } 308 | 309 | if let Ok(vs_path) = env::var("VCINSTALLDIR") { 310 | let arch_path = if target_arch == "x86_64" { 311 | "x64" 312 | } else { 313 | "arm64" 314 | }; 315 | let vs_lib_path = PathBuf::from(vs_path).join("lib").join(arch_path); 316 | println!("cargo:rustc-link-search=native={}", vs_lib_path.display()); 317 | } 318 | 319 | // 重新运行条件 320 | println!("cargo:rerun-if-changed=build.rs"); 321 | println!("cargo:rerun-if-env-changed=VCPKG_ROOT"); 322 | println!("cargo:rerun-if-env-changed=WindowsSdkDir"); 323 | println!("cargo:rerun-if-env-changed=VCINSTALLDIR"); 324 | } 325 | -------------------------------------------------------------------------------- /data/hw_detect.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | **QSV(Intel Quick Sync Video)**、**VAAPI** 和 **CUDA(NVIDIA GPU 加速)** 三种硬件加速方式的检测方法,通过检查硬件、驱动和系统环境来判断支持性: 4 | 5 | --- 6 | 7 | ### **1. QSV(Intel Quick Sync Video)检测** 8 | QSV 是 Intel 核显的硬件编解码技术,依赖 Intel 驱动和媒体 SDK。 9 | 10 | #### **步骤 1:检查 Intel 核显驱动** 11 | ```bash 12 | lspci -nn | grep -Ei "VGA|Display" | grep -i Intel 13 | ``` 14 | 输出示例: 15 | ```bash 16 | 00:02.0 VGA compatible controller [0300]: Intel Corporation HD Graphics 620 [8086:5916] (rev 02) 17 | ``` 18 | 19 | #### **步骤 2:验证内核模块加载** 20 | ```bash 21 | lsmod | grep i915 22 | ``` 23 | 若输出包含 `i915`,说明 Intel 核显驱动已加载。 24 | 25 | #### **步骤 3:检查设备节点** 26 | ```bash 27 | ls -l /dev/dri/ 28 | ``` 29 | 正常输出应包含 `card0` 和 `renderD128`: 30 | ```bash 31 | crw-rw---- 1 root video 226, 0 Aug 1 10:00 card0 32 | crw-rw---- 1 root render 226, 128 Aug 1 10:00 renderD128 33 | ``` 34 | 35 | #### **步骤 4:检查 VAAPI/QSV 支持** 36 | 安装 `vainfo` 工具(如未安装): 37 | ```bash 38 | sudo apt-get install vainfo # Debian/Ubuntu 39 | sudo dnf install vainfo # Fedora 40 | ``` 41 | 运行: 42 | ```bash 43 | vainfo 44 | ``` 45 | 输出示例(显示支持的编解码): 46 | ```bash 47 | VAProfileH264High : VAEntrypointEncSlice # 支持 H.264 编码 48 | VAProfileHEVCMain : VAEntrypointVLD # 支持 HEVC 解码 49 | ``` 50 | 51 | #### **步骤 5:确认 Intel 媒体驱动** 52 | 检查是否安装 Intel 媒体驱动库: 53 | ```bash 54 | ls /usr/lib/x86_64-linux-gnu/libmfx.so* # 检查 Media SDK 库是否存在 55 | ``` 56 | 若存在 `libmfx.so.1` 或类似文件,说明系统支持 QSV。 57 | 58 | --- 59 | 60 | ### **2. VAAPI 检测** 61 | VAAPI 是通用的视频加速接口,支持 Intel、AMD 和部分 NVIDIA 显卡。 62 | 63 | #### **步骤 1:检查显卡支持** 64 | ```bash 65 | lspci -nn | grep -Ei "VGA|Display" 66 | ``` 67 | 根据输出判断显卡类型: 68 | • **Intel**:支持 VAAPI(需 `i915` 驱动) 69 | • **AMD**:支持 VAAPI(需 `amdgpu` 驱动) 70 | • **NVIDIA**:需安装 `nvidia-vaapi-driver`(较新驱动) 71 | 72 | #### **步骤 2:验证驱动加载** 73 | ```bash 74 | lsmod | grep -E "i915|amdgpu|nvidia" 75 | ``` 76 | 根据显卡类型检查驱动模块是否加载。 77 | 78 | #### **步骤 3:运行 `vainfo`** 79 | ```bash 80 | vainfo 81 | ``` 82 | 输出示例: 83 | ```bash 84 | # Intel 核显 85 | VAProfileH264High : VAEntrypointEncSlice 86 | VAProfileHEVCMain : VAEntrypointVLD 87 | 88 | # NVIDIA 显卡(需安装 nvidia-vaapi-driver) 89 | VAProfileH264High : VAEntrypointEncSlice 90 | VAProfileHEVCMain : VAEntrypointVLD 91 | 92 | # AMD 显卡 93 | VAProfileH264High : VAEntrypointVLD 94 | ``` 95 | 96 | #### **步骤 4:检查权限** 97 | 确保用户属于 `video` 和 `render` 组: 98 | ```bash 99 | groups | grep -E "video|render" 100 | ``` 101 | 若无输出,将用户加入组: 102 | ```bash 103 | sudo usermod -aG video,render $USER 104 | ``` 105 | 106 | --- 107 | 108 | ### **3. CUDA 检测** 109 | CUDA 是 NVIDIA 的 GPU 计算加速技术,需安装 NVIDIA 驱动和 CUDA Toolkit。 110 | 111 | #### **步骤 1:检查 NVIDIA 显卡** 112 | ```bash 113 | lspci -nn | grep -i NVIDIA 114 | ``` 115 | 输出示例: 116 | ```bash 117 | 01:00.0 VGA compatible controller [0300]: NVIDIA Corporation GP106 [GeForce GTX 1060] [10de:1b03] (rev a1) 118 | ``` 119 | 120 | #### **步骤 2:验证 NVIDIA 驱动** 121 | ```bash 122 | lsmod | grep nvidia 123 | ``` 124 | 输出应包含 `nvidia`、`nvidia_drm` 或 `nvidia_uvm`。 125 | 126 | #### **步骤 3:检查 CUDA 库** 127 | ```bash 128 | ls /usr/lib/x86_64-linux-gnu/libcuda* # 检查 CUDA 运行时库 129 | ls /usr/local/cuda/ # 检查 CUDA Toolkit 安装路径 130 | ``` 131 | 132 | #### **步骤 4:运行 `nvidia-smi`** 133 | ```bash 134 | nvidia-smi 135 | ``` 136 | 输出示例(显示 GPU 状态和 CUDA 版本): 137 | ```bash 138 | +-----------------------------------------------------------------------------+ 139 | | NVIDIA-SMI 525.60.13 Driver Version: 525.60.13 CUDA Version: 12.0 | 140 | |-------------------------------+----------------------+----------------------+ 141 | ``` 142 | 143 | #### **步骤 5:编译 CUDA 测试程序** 144 | 创建 `cuda_test.cu`: 145 | ```cuda 146 | #include 147 | 148 | __global__ void hello() { 149 | printf("Hello from GPU!\n"); 150 | } 151 | 152 | int main() { 153 | hello<<<1,1>>>(); 154 | cudaDeviceSynchronize(); 155 | return 0; 156 | } 157 | ``` 158 | 编译并运行: 159 | ```bash 160 | nvcc cuda_test.cu -o cuda_test && ./cuda_test 161 | ``` 162 | 若输出 `Hello from GPU!`,说明 CUDA 支持正常。 163 | 164 | --- 165 | 166 | ### **总结表格** 167 | 168 | | 加速技术 | 检测要点 | 支持条件 | 169 | |----------|------------------------------------------------------------------------------------------|--------------------------------------------------------------------------| 170 | | **QSV** | 1. Intel 核显存在且驱动 `i915` 加载
2. `/dev/dri/renderD128` 存在
3. `vainfo` 显示 Intel 编解码 | 需满足所有条件 | 171 | | **VAAPI**| 1. 显卡驱动加载(Intel/AMD/NVIDIA)
2. `vainfo` 显示支持的编解码
3. 用户属于 `video` 组 | 需满足所有条件 | 172 | | **CUDA** | 1. NVIDIA 显卡存在且驱动加载
2. `nvidia-smi` 显示 CUDA 版本
3. CUDA 库和工具链正常 | 需满足所有条件 | 173 | 174 | --- 175 | 176 | ### **常见问题** 177 | 1. **权限问题**:若无法访问 `/dev/dri/renderD128`,将用户加入 `render` 和 `video` 组。 178 | 2. **驱动未安装**: 179 | • Intel/AMD:安装开源驱动(如 `mesa-vdpau-drivers`)。 180 | • NVIDIA:从官网下载驱动或使用 `nvidia-detect`(Debian/Ubuntu)。 181 | 3. **库缺失**: 182 | • VAAPI:安装 `libva2`、`intel-media-va-driver`(Intel)或 `mesa-va-drivers`(AMD)。 183 | • CUDA:安装 `nvidia-cuda-toolkit`。 -------------------------------------------------------------------------------- /data/pixel.txt: -------------------------------------------------------------------------------- 1 | pub const ffi::AV_PIX_FMT_NONE = -1; 2 | pub const ffi::AV_PIX_FMT_YUV420P = 0; 3 | pub const ffi::AV_PIX_FMT_YUYV422 = 1; 4 | pub const ffi::AV_PIX_FMT_RGB24 = 2; 5 | pub const ffi::AV_PIX_FMT_BGR24 = 3; 6 | pub const ffi::AV_PIX_FMT_YUV422P = 4; 7 | pub const ffi::AV_PIX_FMT_YUV444P = 5; 8 | pub const ffi::AV_PIX_FMT_YUV410P = 6; 9 | pub const ffi::AV_PIX_FMT_YUV411P = 7; 10 | pub const ffi::AV_PIX_FMT_GRAY8 = 8; 11 | pub const ffi::AV_PIX_FMT_MONOWHITE = 9; 12 | pub const ffi::AV_PIX_FMT_MONOBLACK = 10; 13 | pub const ffi::AV_PIX_FMT_PAL8 = 11; 14 | pub const ffi::AV_PIX_FMT_YUVJ420P = 12; 15 | pub const ffi::AV_PIX_FMT_YUVJ422P = 13; 16 | pub const ffi::AV_PIX_FMT_YUVJ444P = 14; 17 | pub const ffi::AV_PIX_FMT_UYVY422 = 15; 18 | pub const ffi::AV_PIX_FMT_UYYVYY411 = 16; 19 | pub const ffi::AV_PIX_FMT_BGR8 = 17; 20 | pub const ffi::AV_PIX_FMT_BGR4 = 18; 21 | pub const ffi::AV_PIX_FMT_BGR4_BYTE = 19; 22 | pub const ffi::AV_PIX_FMT_RGB8 = 20; 23 | pub const ffi::AV_PIX_FMT_RGB4 = 21; 24 | pub const ffi::AV_PIX_FMT_RGB4_BYTE = 22; 25 | pub const ffi::AV_PIX_FMT_NV12 = 23; 26 | pub const ffi::AV_PIX_FMT_NV21 = 24; 27 | pub const ffi::AV_PIX_FMT_ARGB = 25; 28 | pub const ffi::AV_PIX_FMT_RGBA = 26; 29 | pub const ffi::AV_PIX_FMT_ABGR = 27; 30 | pub const ffi::AV_PIX_FMT_BGRA = 28; 31 | pub const ffi::AV_PIX_FMT_GRAY16BE = 29; 32 | pub const ffi::AV_PIX_FMT_GRAY16LE = 30; 33 | pub const ffi::AV_PIX_FMT_YUV440P = 31; 34 | pub const ffi::AV_PIX_FMT_YUVJ440P = 32; 35 | pub const ffi::AV_PIX_FMT_YUVA420P = 33; 36 | pub const ffi::AV_PIX_FMT_RGB48BE = 34; 37 | pub const ffi::AV_PIX_FMT_RGB48LE = 35; 38 | pub const ffi::AV_PIX_FMT_RGB565BE = 36; 39 | pub const ffi::AV_PIX_FMT_RGB565LE = 37; 40 | pub const ffi::AV_PIX_FMT_RGB555BE = 38; 41 | pub const ffi::AV_PIX_FMT_RGB555LE = 39; 42 | pub const ffi::AV_PIX_FMT_BGR565BE = 40; 43 | pub const ffi::AV_PIX_FMT_BGR565LE = 41; 44 | pub const ffi::AV_PIX_FMT_BGR555BE = 42; 45 | pub const ffi::AV_PIX_FMT_BGR555LE = 43; 46 | pub const ffi::AV_PIX_FMT_VAAPI = 44; 47 | pub const ffi::AV_PIX_FMT_YUV420P16LE = 45; 48 | pub const ffi::AV_PIX_FMT_YUV420P16BE = 46; 49 | pub const ffi::AV_PIX_FMT_YUV422P16LE = 47; 50 | pub const ffi::AV_PIX_FMT_YUV422P16BE = 48; 51 | pub const ffi::AV_PIX_FMT_YUV444P16LE = 49; 52 | pub const ffi::AV_PIX_FMT_YUV444P16BE = 50; 53 | pub const ffi::AV_PIX_FMT_DXVA2_VLD = 51; 54 | pub const ffi::AV_PIX_FMT_RGB444LE = 52; 55 | pub const ffi::AV_PIX_FMT_RGB444BE = 53; 56 | pub const ffi::AV_PIX_FMT_BGR444LE = 54; 57 | pub const ffi::AV_PIX_FMT_BGR444BE = 55; 58 | pub const ffi::AV_PIX_FMT_YA8 = 56; 59 | pub const ffi::AV_PIX_FMT_Y400A = 56; 60 | pub const ffi::AV_PIX_FMT_GRAY8A = 56; 61 | pub const ffi::AV_PIX_FMT_BGR48BE = 57; 62 | pub const ffi::AV_PIX_FMT_BGR48LE = 58; 63 | pub const ffi::AV_PIX_FMT_YUV420P9BE = 59; 64 | pub const ffi::AV_PIX_FMT_YUV420P9LE = 60; 65 | pub const ffi::AV_PIX_FMT_YUV420P10BE = 61; 66 | pub const ffi::AV_PIX_FMT_YUV420P10LE = 62; 67 | pub const ffi::AV_PIX_FMT_YUV422P10BE = 63; 68 | pub const ffi::AV_PIX_FMT_YUV422P10LE = 64; 69 | pub const ffi::AV_PIX_FMT_YUV444P9BE = 65; 70 | pub const ffi::AV_PIX_FMT_YUV444P9LE = 66; 71 | pub const ffi::AV_PIX_FMT_YUV444P10BE = 67; 72 | pub const ffi::AV_PIX_FMT_YUV444P10LE = 68; 73 | pub const ffi::AV_PIX_FMT_YUV422P9BE = 69; 74 | pub const ffi::AV_PIX_FMT_YUV422P9LE = 70; 75 | pub const ffi::AV_PIX_FMT_GBRP = 71; 76 | pub const ffi::AV_PIX_FMT_GBR24P = 71; 77 | pub const ffi::AV_PIX_FMT_GBRP9BE = 72; 78 | pub const ffi::AV_PIX_FMT_GBRP9LE = 73; 79 | pub const ffi::AV_PIX_FMT_GBRP10BE = 74; 80 | pub const ffi::AV_PIX_FMT_GBRP10LE = 75; 81 | pub const ffi::AV_PIX_FMT_GBRP16BE = 76; 82 | pub const ffi::AV_PIX_FMT_GBRP16LE = 77; 83 | pub const ffi::AV_PIX_FMT_YUVA422P = 78; 84 | pub const ffi::AV_PIX_FMT_YUVA444P = 79; 85 | pub const ffi::AV_PIX_FMT_YUVA420P9BE = 80; 86 | pub const ffi::AV_PIX_FMT_YUVA420P9LE = 81; 87 | pub const ffi::AV_PIX_FMT_YUVA422P9BE = 82; 88 | pub const ffi::AV_PIX_FMT_YUVA422P9LE = 83; 89 | pub const ffi::AV_PIX_FMT_YUVA444P9BE = 84; 90 | pub const ffi::AV_PIX_FMT_YUVA444P9LE = 85; 91 | pub const ffi::AV_PIX_FMT_YUVA420P10BE = 86; 92 | pub const ffi::AV_PIX_FMT_YUVA420P10LE = 87; 93 | pub const ffi::AV_PIX_FMT_YUVA422P10BE = 88; 94 | pub const ffi::AV_PIX_FMT_YUVA422P10LE = 89; 95 | pub const ffi::AV_PIX_FMT_YUVA444P10BE = 90; 96 | pub const ffi::AV_PIX_FMT_YUVA444P10LE = 91; 97 | pub const ffi::AV_PIX_FMT_YUVA420P16BE = 92; 98 | pub const ffi::AV_PIX_FMT_YUVA420P16LE = 93; 99 | pub const ffi::AV_PIX_FMT_YUVA422P16BE = 94; 100 | pub const ffi::AV_PIX_FMT_YUVA422P16LE = 95; 101 | pub const ffi::AV_PIX_FMT_YUVA444P16BE = 96; 102 | pub const ffi::AV_PIX_FMT_YUVA444P16LE = 97; 103 | pub const ffi::AV_PIX_FMT_VDPAU = 98; 104 | pub const ffi::AV_PIX_FMT_XYZ12LE = 99; 105 | pub const ffi::AV_PIX_FMT_XYZ12BE = 100; 106 | pub const ffi::AV_PIX_FMT_NV16 = 101; 107 | pub const ffi::AV_PIX_FMT_NV20LE = 102; 108 | pub const ffi::AV_PIX_FMT_NV20BE = 103; 109 | pub const ffi::AV_PIX_FMT_RGBA64BE = 104; 110 | pub const ffi::AV_PIX_FMT_RGBA64LE = 105; 111 | pub const ffi::AV_PIX_FMT_BGRA64BE = 106; 112 | pub const ffi::AV_PIX_FMT_BGRA64LE = 107; 113 | pub const ffi::AV_PIX_FMT_YVYU422 = 108; 114 | pub const ffi::AV_PIX_FMT_YA16BE = 109; 115 | pub const ffi::AV_PIX_FMT_YA16LE = 110; 116 | pub const ffi::AV_PIX_FMT_GBRAP = 111; 117 | pub const ffi::AV_PIX_FMT_GBRAP16BE = 112; 118 | pub const ffi::AV_PIX_FMT_GBRAP16LE = 113; 119 | pub const ffi::AV_PIX_FMT_QSV = 114; 120 | pub const ffi::AV_PIX_FMT_MMAL = 115; 121 | pub const ffi::AV_PIX_FMT_D3D11VA_VLD = 116; 122 | pub const ffi::AV_PIX_FMT_CUDA = 117; 123 | pub const ffi::AV_PIX_FMT_0RGB = 118; 124 | pub const ffi::AV_PIX_FMT_RGB0 = 119; 125 | pub const ffi::AV_PIX_FMT_0BGR = 120; 126 | pub const ffi::AV_PIX_FMT_BGR0 = 121; 127 | pub const ffi::AV_PIX_FMT_YUV420P12BE = 122; 128 | pub const ffi::AV_PIX_FMT_YUV420P12LE = 123; 129 | pub const ffi::AV_PIX_FMT_YUV420P14BE = 124; 130 | pub const ffi::AV_PIX_FMT_YUV420P14LE = 125; 131 | pub const ffi::AV_PIX_FMT_YUV422P12BE = 126; 132 | pub const ffi::AV_PIX_FMT_YUV422P12LE = 127; 133 | pub const ffi::AV_PIX_FMT_YUV422P14BE = 128; 134 | pub const ffi::AV_PIX_FMT_YUV422P14LE = 129; 135 | pub const ffi::AV_PIX_FMT_YUV444P12BE = 130; 136 | pub const ffi::AV_PIX_FMT_YUV444P12LE = 131; 137 | pub const ffi::AV_PIX_FMT_YUV444P14BE = 132; 138 | pub const ffi::AV_PIX_FMT_YUV444P14LE = 133; 139 | pub const ffi::AV_PIX_FMT_GBRP12BE = 134; 140 | pub const ffi::AV_PIX_FMT_GBRP12LE = 135; 141 | pub const ffi::AV_PIX_FMT_GBRP14BE = 136; 142 | pub const ffi::AV_PIX_FMT_GBRP14LE = 137; 143 | pub const ffi::AV_PIX_FMT_YUVJ411P = 138; 144 | pub const ffi::AV_PIX_FMT_BAYER_BGGR8 = 139; 145 | pub const ffi::AV_PIX_FMT_BAYER_RGGB8 = 140; 146 | pub const ffi::AV_PIX_FMT_BAYER_GBRG8 = 141; 147 | pub const ffi::AV_PIX_FMT_BAYER_GRBG8 = 142; 148 | pub const ffi::AV_PIX_FMT_BAYER_BGGR16L = 143; 149 | pub const ffi::AV_PIX_FMT_BAYER_BGGR16B = 144; 150 | pub const ffi::AV_PIX_FMT_BAYER_RGGB16L = 145; 151 | pub const ffi::AV_PIX_FMT_BAYER_RGGB16B = 146; 152 | pub const ffi::AV_PIX_FMT_BAYER_GBRG16L = 147; 153 | pub const ffi::AV_PIX_FMT_BAYER_GBRG16B = 148; 154 | pub const ffi::AV_PIX_FMT_BAYER_GRBG16L = 149; 155 | pub const ffi::AV_PIX_FMT_BAYER_GRBG16B = 150; 156 | pub const ffi::AV_PIX_FMT_YUV440P10LE = 151; 157 | pub const ffi::AV_PIX_FMT_YUV440P10BE = 152; 158 | pub const ffi::AV_PIX_FMT_YUV440P12LE = 153; 159 | pub const ffi::AV_PIX_FMT_YUV440P12BE = 154; 160 | pub const ffi::AV_PIX_FMT_AYUV64LE = 155; 161 | pub const ffi::AV_PIX_FMT_AYUV64BE = 156; 162 | pub const ffi::AV_PIX_FMT_VIDEOTOOLBOX = 157; 163 | pub const ffi::AV_PIX_FMT_P010LE = 158; 164 | pub const ffi::AV_PIX_FMT_P010BE = 159; 165 | pub const ffi::AV_PIX_FMT_GBRAP12BE = 160; 166 | pub const ffi::AV_PIX_FMT_GBRAP12LE = 161; 167 | pub const ffi::AV_PIX_FMT_GBRAP10BE = 162; 168 | pub const ffi::AV_PIX_FMT_GBRAP10LE = 163; 169 | pub const ffi::AV_PIX_FMT_MEDIACODEC = 164; 170 | pub const ffi::AV_PIX_FMT_GRAY12BE = 165; 171 | pub const ffi::AV_PIX_FMT_GRAY12LE = 166; 172 | pub const ffi::AV_PIX_FMT_GRAY10BE = 167; 173 | pub const ffi::AV_PIX_FMT_GRAY10LE = 168; 174 | pub const ffi::AV_PIX_FMT_P016LE = 169; 175 | pub const ffi::AV_PIX_FMT_P016BE = 170; 176 | pub const ffi::AV_PIX_FMT_D3D11 = 171; 177 | pub const ffi::AV_PIX_FMT_GRAY9BE = 172; 178 | pub const ffi::AV_PIX_FMT_GRAY9LE = 173; 179 | pub const ffi::AV_PIX_FMT_GBRPF32BE = 174; 180 | pub const ffi::AV_PIX_FMT_GBRPF32LE = 175; 181 | pub const ffi::AV_PIX_FMT_GBRAPF32BE = 176; 182 | pub const ffi::AV_PIX_FMT_GBRAPF32LE = 177; 183 | pub const ffi::AV_PIX_FMT_DRM_PRIME = 178; 184 | pub const ffi::AV_PIX_FMT_OPENCL = 179; 185 | pub const ffi::AV_PIX_FMT_GRAY14BE = 180; 186 | pub const ffi::AV_PIX_FMT_GRAY14LE = 181; 187 | pub const ffi::AV_PIX_FMT_GRAYF32BE = 182; 188 | pub const ffi::AV_PIX_FMT_GRAYF32LE = 183; 189 | pub const ffi::AV_PIX_FMT_YUVA422P12BE = 184; 190 | pub const ffi::AV_PIX_FMT_YUVA422P12LE = 185; 191 | pub const ffi::AV_PIX_FMT_YUVA444P12BE = 186; 192 | pub const ffi::AV_PIX_FMT_YUVA444P12LE = 187; 193 | pub const ffi::AV_PIX_FMT_NV24 = 188; 194 | pub const ffi::AV_PIX_FMT_NV42 = 189; 195 | pub const ffi::AV_PIX_FMT_VULKAN = 190; 196 | pub const ffi::AV_PIX_FMT_Y210BE = 191; 197 | pub const ffi::AV_PIX_FMT_Y210LE = 192; 198 | pub const ffi::AV_PIX_FMT_X2RGB10LE = 193; 199 | pub const ffi::AV_PIX_FMT_X2RGB10BE = 194; 200 | pub const ffi::AV_PIX_FMT_X2BGR10LE = 195; 201 | pub const ffi::AV_PIX_FMT_X2BGR10BE = 196; 202 | pub const ffi::AV_PIX_FMT_P210BE = 197; 203 | pub const ffi::AV_PIX_FMT_P210LE = 198; 204 | pub const ffi::AV_PIX_FMT_P410BE = 199; 205 | pub const ffi::AV_PIX_FMT_P410LE = 200; 206 | pub const ffi::AV_PIX_FMT_P216BE = 201; 207 | pub const ffi::AV_PIX_FMT_P216LE = 202; 208 | pub const ffi::AV_PIX_FMT_P416BE = 203; 209 | pub const ffi::AV_PIX_FMT_P416LE = 204; 210 | pub const ffi::AV_PIX_FMT_VUYA = 205; 211 | pub const ffi::AV_PIX_FMT_RGBAF16BE = 206; 212 | pub const ffi::AV_PIX_FMT_RGBAF16LE = 207; 213 | pub const ffi::AV_PIX_FMT_VUYX = 208; 214 | pub const ffi::AV_PIX_FMT_P012LE = 209; 215 | pub const ffi::AV_PIX_FMT_P012BE = 210; 216 | pub const ffi::AV_PIX_FMT_Y212BE = 211; 217 | pub const ffi::AV_PIX_FMT_Y212LE = 212; 218 | pub const ffi::AV_PIX_FMT_XV30BE = 213; 219 | pub const ffi::AV_PIX_FMT_XV30LE = 214; 220 | pub const ffi::AV_PIX_FMT_XV36BE = 215; 221 | pub const ffi::AV_PIX_FMT_XV36LE = 216; 222 | pub const ffi::AV_PIX_FMT_RGBF32BE = 217; 223 | pub const ffi::AV_PIX_FMT_RGBF32LE = 218; 224 | pub const ffi::AV_PIX_FMT_RGBAF32BE = 219; 225 | pub const ffi::AV_PIX_FMT_RGBAF32LE = 220; 226 | pub const ffi::AV_PIX_FMT_P212BE = 221; 227 | pub const ffi::AV_PIX_FMT_P212LE = 222; 228 | pub const ffi::AV_PIX_FMT_P412BE = 223; 229 | pub const ffi::AV_PIX_FMT_P412LE = 224; 230 | pub const ffi::AV_PIX_FMT_GBRAP14BE = 225; 231 | pub const ffi::AV_PIX_FMT_GBRAP14LE = 226; 232 | pub const ffi::AV_PIX_FMT_D3D12 = 227; 233 | pub const ffi::AV_PIX_FMT_NB = 228; 234 | -------------------------------------------------------------------------------- /examples/audio_mix_sources.rs: -------------------------------------------------------------------------------- 1 | use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; 2 | use rodio::source::{SineWave, Source}; 3 | use rodio::{dynamic_mixer, OutputStream, Sink}; 4 | use std::sync::{mpsc, Arc, Mutex}; 5 | use std::time::Duration; 6 | 7 | /// 音频处理器结构体 8 | #[derive(Debug)] 9 | #[allow(dead_code)] 10 | struct AudioProcessor { 11 | sample_rate: u32, 12 | channels: usize, 13 | buffer: Arc>>, 14 | volume: f32, 15 | } 16 | 17 | /// 音频分析结果 18 | #[derive(Debug)] 19 | struct AudioAnalysis { 20 | max_amplitude: f32, 21 | rms: f32, 22 | } 23 | 24 | impl AudioProcessor { 25 | fn new(channels: usize, sample_rate: u32) -> Self { 26 | Self { 27 | sample_rate, 28 | channels, 29 | buffer: Arc::new(Mutex::new(Vec::new())), 30 | volume: 1.0, 31 | } 32 | } 33 | 34 | fn set_volume(&mut self, volume: f32) { 35 | self.volume = volume.clamp(0.0, 1.0); 36 | } 37 | 38 | fn apply_effects(&self, data: &mut [f32]) { 39 | for sample in data.iter_mut() { 40 | *sample *= self.volume; 41 | } 42 | } 43 | 44 | fn analyze_audio(&self, data: &[f32]) -> AudioAnalysis { 45 | let mut max_amplitude = 0.0f32; 46 | let mut rms = 0.0f32; 47 | 48 | for &sample in data { 49 | max_amplitude = max_amplitude.max(sample.abs()); 50 | rms += sample * sample; 51 | } 52 | 53 | rms = (rms / data.len() as f32).sqrt(); 54 | 55 | AudioAnalysis { max_amplitude, rms } 56 | } 57 | 58 | fn process_buffer(&self, buffer: &mut [f32]) { 59 | self.apply_effects(buffer); 60 | let analysis = self.analyze_audio(buffer); 61 | println!( 62 | "Audio Analysis - Max Amplitude: {:.2}, RMS: {:.2}", 63 | analysis.max_amplitude, analysis.rms 64 | ); 65 | } 66 | } 67 | 68 | fn main() -> Result<(), Box> { 69 | // 1. 创建音频处理器 70 | let audio_processor = Arc::new(Mutex::new(AudioProcessor::new(2, 44_100))); 71 | 72 | // 2. Rodio 设置 73 | let (controller, mixer) = dynamic_mixer::mixer::(2, 44_100); 74 | let (_stream, stream_handle) = OutputStream::try_default().unwrap(); 75 | let sink = Sink::try_new(&stream_handle)?; 76 | 77 | // 3. CPAL 设置 78 | let host = cpal::default_host(); 79 | let device = host 80 | .default_output_device() 81 | .expect("Failed to get default output device"); 82 | 83 | println!("Using audio device: {}", device.name()?); 84 | let config = device.default_output_config()?; 85 | println!("Default output config: {:?}", config); 86 | 87 | // 4. 创建音频源 88 | // Create four unique sources. The frequencies used here correspond 89 | // notes in the key of C and in octave 4: C4, or middle C on a piano, 90 | // E4, G4, and A4 respectively. 91 | let source_c = SineWave::new(261.63) 92 | .take_duration(Duration::from_secs_f32(5.)) 93 | .amplify(0.20); 94 | let source_e = SineWave::new(329.63) 95 | .take_duration(Duration::from_secs_f32(5.)) 96 | .amplify(0.20); 97 | let source_g = SineWave::new(392.0) 98 | .take_duration(Duration::from_secs_f32(5.)) 99 | .amplify(0.20); 100 | let source_a = SineWave::new(440.0) 101 | .take_duration(Duration::from_secs_f32(5.)) 102 | .amplify(0.20); 103 | 104 | // 5. 设置通信通道 105 | let (tx, rx) = mpsc::channel::>(); 106 | let processor_clone = audio_processor.clone(); 107 | 108 | // 6. 启动音频处理线程 109 | std::thread::spawn(move || { 110 | while let Ok(mut buffer) = rx.recv() { 111 | processor_clone.lock().unwrap().process_buffer(&mut buffer); 112 | } 113 | }); 114 | 115 | // 7. 创建 CPAL 音频流 116 | let stream = match config.sample_format() { 117 | cpal::SampleFormat::F32 => { 118 | create_stream::(&device, &config.into(), tx.clone(), audio_processor.clone()) 119 | } 120 | cpal::SampleFormat::I16 => { 121 | create_stream::(&device, &config.into(), tx.clone(), audio_processor.clone()) 122 | } 123 | cpal::SampleFormat::U16 => { 124 | create_stream::(&device, &config.into(), tx.clone(), audio_processor.clone()) 125 | } 126 | _ => panic!("Unsupported sample format"), 127 | }?; 128 | 129 | // 8. 添加音源到混音器 130 | controller.add(source_c); 131 | controller.add(source_e); 132 | controller.add(source_g); 133 | controller.add(source_a); 134 | sink.append(mixer); 135 | 136 | // 9. 启动音频流和音频处理 137 | stream.play().unwrap(); 138 | 139 | // 10. 演示音量控制 140 | std::thread::spawn(move || { 141 | // 等待一下,确保音频开始播放 142 | std::thread::sleep(Duration::from_secs(1)); 143 | 144 | for volume in (0..=10).map(|v| v as f32 / 10.0) { 145 | std::thread::sleep(Duration::from_millis(1000)); 146 | audio_processor.lock().unwrap().set_volume(volume); 147 | println!("Setting volume to: {:.1}", volume); 148 | } 149 | }); 150 | 151 | println!("Playing audio... Press Ctrl+C to stop"); 152 | 153 | // 11. 等待播放完成 154 | sink.sleep_until_end(); 155 | 156 | Ok(()) 157 | } 158 | 159 | fn create_stream( 160 | device: &cpal::Device, 161 | config: &cpal::StreamConfig, 162 | tx: mpsc::Sender>, 163 | processor: Arc>, 164 | ) -> Result 165 | where 166 | T: cpal::Sample + cpal::SizedSample + rodio::Sample, 167 | { 168 | let channels = config.channels as usize; 169 | 170 | device.build_output_stream( 171 | config, 172 | move |data: &mut [T], _: &cpal::OutputCallbackInfo| { 173 | process_audio_data(data, channels, &tx, &processor); 174 | }, 175 | move |err| { 176 | eprintln!("Error in audio stream: {:?}", err); 177 | }, 178 | None, 179 | ) 180 | } 181 | 182 | fn process_audio_data( 183 | data: &mut [T], 184 | channels: usize, 185 | tx: &mpsc::Sender>, 186 | processor: &Arc>, 187 | ) where 188 | T: cpal::Sample + cpal::SizedSample + rodio::Sample, 189 | { 190 | let mut audio_buffer = Vec::with_capacity(data.len()); 191 | 192 | for frame in data.chunks_mut(channels) { 193 | for sample in frame.iter_mut() { 194 | audio_buffer.push(sample.to_f32()); 195 | } 196 | } 197 | 198 | // 应用音频处理 199 | if let Ok(proc) = processor.lock() { 200 | proc.apply_effects(&mut audio_buffer); 201 | } 202 | 203 | // 发送处理后的音频数据 204 | tx.send(audio_buffer).unwrap_or_else(|err| { 205 | eprintln!("Error sending audio data: {:?}", err); 206 | }); 207 | } 208 | 209 | #[cfg(test)] 210 | mod tests { 211 | use super::*; 212 | 213 | #[test] 214 | fn test_audio_processor() { 215 | let processor = AudioProcessor::new(2, 44100); 216 | let mut test_data = vec![0.5f32; 1024]; 217 | 218 | // 测试音量调整 219 | processor.apply_effects(&mut test_data); 220 | assert!(test_data.iter().all(|&x| x <= 0.5)); 221 | 222 | // 测试音频分析 223 | let analysis = processor.analyze_audio(&test_data); 224 | assert!(analysis.max_amplitude <= 0.5); 225 | assert!(analysis.rms > 0.0); 226 | } 227 | 228 | #[test] 229 | fn test_audio_stream() { 230 | let host = cpal::default_host(); 231 | let device = host.default_output_device().unwrap(); 232 | let config = device.default_output_config().unwrap(); 233 | let (tx, _rx) = mpsc::channel(); 234 | let processor = Arc::new(Mutex::new(AudioProcessor::new(2, 44100))); 235 | 236 | let stream = match config.sample_format() { 237 | cpal::SampleFormat::F32 => { 238 | create_stream::(&device, &config.into(), tx.clone(), processor) 239 | } 240 | cpal::SampleFormat::I16 => { 241 | create_stream::(&device, &config.into(), tx.clone(), processor) 242 | } 243 | cpal::SampleFormat::U16 => { 244 | create_stream::(&device, &config.into(), tx.clone(), processor) 245 | } 246 | _ => panic!("不支持的采样格式"), 247 | }; 248 | 249 | assert!(stream.is_ok(), "音频流创建失败"); 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /examples/audio_recorder.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use chrono::Local; 3 | use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; 4 | use cpal::{Device, SampleFormat, Stream, StreamConfig}; 5 | use rodio::{Decoder, Sink}; 6 | use std::fs::File; 7 | use std::io::{BufReader, Write}; 8 | use std::path::Path; 9 | use std::sync::atomic::{AtomicBool, Ordering}; 10 | use std::sync::{Arc, Mutex}; 11 | use std::time::Instant; 12 | use wavers::Wav; 13 | 14 | /// 录音器结构体 15 | pub struct AudioRecorder { 16 | device: Device, 17 | config: StreamConfig, 18 | stream: Option, 19 | running: Arc, 20 | recording_buffer: Arc>>, 21 | start_time: Arc>>, 22 | } 23 | 24 | impl AudioRecorder { 25 | /// 创建新的录音器实例 26 | pub fn new() -> Result { 27 | let host = cpal::default_host(); 28 | 29 | let device = host 30 | .default_input_device() 31 | .ok_or_else(|| anyhow::anyhow!("未找到输入设备"))?; 32 | 33 | println!("使用输入设备: {}", device.name()?); 34 | 35 | let config: StreamConfig = device.default_input_config()?.into(); 36 | 37 | println!("采样率: {} Hz", config.sample_rate.0); 38 | println!("通道数: {}", config.channels); 39 | 40 | Ok(Self { 41 | device, 42 | config, 43 | stream: None, 44 | recording_buffer: Arc::new(Mutex::new(Vec::new())), 45 | running: Arc::new(AtomicBool::new(false)), 46 | start_time: Arc::new(Mutex::new(None)), 47 | }) 48 | } 49 | 50 | /// 开始录音 51 | pub fn start_recording(&mut self) -> Result<()> { 52 | let recording_buffer = Arc::clone(&self.recording_buffer); 53 | let running = Arc::clone(&self.running); 54 | let start_time = Arc::clone(&self.start_time); 55 | 56 | self.running.store(true, Ordering::SeqCst); 57 | *start_time.lock().unwrap() = Some(Instant::now()); 58 | 59 | let stream = match self.device.default_input_config()?.sample_format() { 60 | SampleFormat::F32 => self.build_stream::(&recording_buffer, &running)?, 61 | SampleFormat::I16 => self.build_stream::(&recording_buffer, &running)?, 62 | SampleFormat::U16 => self.build_stream::(&recording_buffer, &running)?, 63 | _ => return Err(anyhow::anyhow!("不支持的采样格式")), 64 | }; 65 | 66 | stream.play()?; 67 | self.stream = Some(stream); 68 | 69 | Ok(()) 70 | } 71 | 72 | /// 停止录音 73 | pub fn stop_recording(&mut self) { 74 | self.running.store(false, Ordering::SeqCst); 75 | self.stream = None; 76 | } 77 | 78 | /// 构建音频流 79 | fn build_stream( 80 | &self, 81 | recording_buffer: &Arc>>, 82 | running: &Arc, 83 | ) -> Result 84 | where 85 | T: cpal::Sample + cpal::SizedSample, 86 | { 87 | let err_fn = |err| eprintln!("输入音频流发生错误: {}", err); 88 | 89 | let recording_buffer = Arc::clone(recording_buffer); 90 | let running = Arc::clone(running); 91 | 92 | let stream = self.device.build_input_stream( 93 | &self.config, 94 | move |data: &[T], _: &_| { 95 | if running.load(Ordering::SeqCst) { 96 | let mut buffer = recording_buffer.lock().unwrap(); 97 | // 使用 Sample trait 的方法转换为 f32 98 | for &sample in data.iter() { 99 | buffer.push(sample.to_float_sample()); 100 | } 101 | } 102 | }, 103 | err_fn, 104 | None, 105 | )?; 106 | 107 | Ok(stream) 108 | } 109 | 110 | /// 获取录音时长(秒) 111 | pub fn get_duration(&self) -> f32 { 112 | if let Some(start_time) = *self.start_time.lock().unwrap() { 113 | start_time.elapsed().as_secs_f32() 114 | } else { 115 | 0.0 116 | } 117 | } 118 | 119 | /// 是否正在录音 120 | pub fn is_recording(&self) -> bool { 121 | self.running.load(Ordering::SeqCst) 122 | } 123 | 124 | /// 保存录音到WAV文件 125 | pub fn save_to_wav(&self, filename: &str) -> Result<()> { 126 | let samples = self.recording_buffer.lock().unwrap(); 127 | let sample_rate = self.config.sample_rate.0 as i32; 128 | let n_channels = self.config.channels; 129 | 130 | // 使用 wavers 的 write 函数保存 WAV 文件 131 | wavers::write(Path::new(filename), &samples, sample_rate, n_channels)?; 132 | 133 | Ok(()) 134 | } 135 | 136 | /// 获取当前音量级别 (RMS值) 137 | /// dB是标准的音频测量单位 138 | /// 常见的dB值范围: 139 | /// 0 dB: 参考值 140 | /// -60 dB: 很安静 141 | /// -40 dB: 低音量 142 | /// -20 dB: 中等音量 143 | /// 0 dB: 满量程(最大无失真音量) 144 | pub fn get_volume_level(&self) -> f32 { 145 | let buffer = self.recording_buffer.lock().unwrap(); 146 | if buffer.is_empty() { 147 | return 0.0; 148 | } 149 | 150 | // 取最后1024个采样计算RMS值 151 | let window_size = 1024.min(buffer.len()); 152 | let samples = &buffer[buffer.len() - window_size..]; 153 | 154 | // 计算RMS值 155 | let (sum_squares, peak) = samples.iter().fold((0.0f32, 0.0f32), |(sum, peak), &x| { 156 | let x_abs = x.abs(); 157 | (sum + x * x, peak.max(x_abs)) 158 | }); 159 | 160 | let rms = (sum_squares / window_size as f32).sqrt(); 161 | 162 | // 混合RMS和峰值以获得更好的响应 163 | let mixed = 0.8 * rms + 0.2 * peak; 164 | 165 | // 将RMS值转换为分贝 (dB) 166 | let db = 20.0 * mixed.log10(); 167 | 168 | // 将分贝值归一化到0-1范围 169 | // 假设-60dB是最小可听值,0dB是最大值 170 | let normalized = (db + 60.0) / 60.0; 171 | normalized.max(0.0).min(1.0) 172 | } 173 | } 174 | 175 | /// 录音控制器 176 | pub struct RecordingController { 177 | recorder: AudioRecorder, 178 | } 179 | 180 | impl RecordingController { 181 | pub fn new() -> Result { 182 | Ok(Self { 183 | recorder: AudioRecorder::new()?, 184 | }) 185 | } 186 | 187 | /// 开始录音,等待手动停止 188 | pub fn start_recording(&mut self) -> Result<()> { 189 | println!("开始录音... (按回车键停止)"); 190 | self.recorder.start_recording()?; 191 | 192 | // 创建一个通道来通知录音停止 193 | let (tx, rx) = std::sync::mpsc::channel(); 194 | 195 | // 在单独的线程中处理用户输入 196 | std::thread::spawn(move || { 197 | let mut input = String::new(); 198 | std::io::stdin().read_line(&mut input).ok(); 199 | tx.send(()).ok(); 200 | }); 201 | 202 | // 主循环更新显示 203 | while self.recorder.is_recording() { 204 | let volume = self.recorder.get_volume_level(); 205 | let volume_bar = Self::create_volume_bar(volume); 206 | 207 | // dB = 20 * log10 (振幅比) 208 | // 将RMS值转换为dB 209 | print!( 210 | "\r录音时长: {:.1}s | 音量: {} {:.2} dB", 211 | self.recorder.get_duration(), 212 | volume_bar, 213 | 20.0 * volume.log10() 214 | ); 215 | 216 | std::io::stdout().flush()?; 217 | 218 | // 非阻塞地检查是否收到停止信号 219 | if rx.try_recv().is_ok() { 220 | break; 221 | } 222 | 223 | // 短暂休眠以减少 CPU 使用 224 | std::thread::sleep(std::time::Duration::from_millis(100)); 225 | } 226 | 227 | self.recorder.stop_recording(); 228 | 229 | let filename = format!("recording_{}.wav", Local::now().format("%Y%m%d_%H%M%S")); 230 | 231 | println!("\n正在保存录音..."); 232 | self.recorder.save_to_wav(&filename)?; 233 | println!("录音已保存到: {}", filename); 234 | 235 | // 显示录音文件信息 236 | let wav = Wav::::from_path(&filename)?; 237 | println!("时长: {:.2} 秒", wav.duration()); 238 | 239 | Ok(()) 240 | } 241 | 242 | /// 创建音量条 243 | fn create_volume_bar(volume: f32) -> String { 244 | let bar_length = (volume * 50.0) as usize; 245 | let bar: String = std::iter::repeat('█') 246 | .take(bar_length) 247 | .chain(std::iter::repeat('░').take(50 - bar_length)) 248 | .collect(); 249 | bar 250 | } 251 | 252 | /// 播放录音 253 | pub fn play_recording(&self, filename: &str) -> Result<()> { 254 | // 首先读取并显示 WAV 文件信息 255 | let wav = Wav::::from_path(filename)?; 256 | println!("正在播放录音文件:"); 257 | println!("采样率: {} Hz", wav.sample_rate()); 258 | println!("通道数: {}", wav.n_channels()); 259 | println!("时长: {:.2} 秒", wav.duration()); 260 | 261 | // 使用 rodio 播放音频 262 | let (_stream, stream_handle) = rodio::OutputStream::try_default()?; 263 | let sink = Sink::try_new(&stream_handle)?; 264 | 265 | let file = File::open(filename)?; 266 | let source = Decoder::new(BufReader::new(file))?; 267 | sink.append(source); 268 | 269 | println!("播放中... (按 Ctrl+C 停止)"); 270 | sink.sleep_until_end(); 271 | Ok(()) 272 | } 273 | } 274 | 275 | fn main() -> Result<()> { 276 | let mut controller = RecordingController::new()?; 277 | 278 | // 开始录音 279 | controller.start_recording()?; 280 | 281 | // 查找并播放最新录制的音频 282 | if let Ok(entries) = std::fs::read_dir(".") { 283 | if let Some(latest_recording) = entries 284 | .filter_map(|entry| entry.ok()) 285 | .filter(|e| { 286 | e.path() 287 | .extension() 288 | .map(|ext| ext == "wav") 289 | .unwrap_or(false) 290 | }) 291 | .max_by_key(|e| e.metadata().unwrap().modified().unwrap()) 292 | { 293 | println!("\n播放录音..."); 294 | controller.play_recording(latest_recording.path().to_str().unwrap())?; 295 | } 296 | } 297 | 298 | Ok(()) 299 | } 300 | -------------------------------------------------------------------------------- /examples/colorous_render.rs: -------------------------------------------------------------------------------- 1 | use colorous::*; 2 | use image::ImageBuffer; 3 | use std::process; 4 | 5 | const GRADIENTS: [(Gradient, &str); 38] = [ 6 | // Sequential (multi-hue) 7 | (TURBO, "TURBO"), 8 | (VIRIDIS, "VIRIDIS"), 9 | (INFERNO, "INFERNO"), 10 | (MAGMA, "MAGMA"), 11 | (PLASMA, "PLASMA"), 12 | (CIVIDIS, "CIVIDIS"), 13 | (WARM, "WARM"), 14 | (COOL, "COOL"), 15 | (CUBEHELIX, "CUBEHELIX"), 16 | (BLUE_GREEN, "BLUE_GREEN"), 17 | (BLUE_PURPLE, "BLUE_PURPLE"), 18 | (GREEN_BLUE, "GREEN_BLUE"), 19 | (ORANGE_RED, "ORANGE_RED"), 20 | (PURPLE_BLUE_GREEN, "PURPLE_BLUE_GREEN"), 21 | (PURPLE_BLUE, "PURPLE_BLUE"), 22 | (PURPLE_RED, "PURPLE_RED"), 23 | (RED_PURPLE, "RED_PURPLE"), 24 | (YELLOW_GREEN_BLUE, "YELLOW_GREEN_BLUE"), 25 | (YELLOW_GREEN, "YELLOW_GREEN"), 26 | (YELLOW_ORANGE_BROWN, "YELLOW_ORANGE_BROWN"), 27 | (YELLOW_ORANGE_RED, "YELLOW_ORANGE_RED"), 28 | // Sequential (single-hue) 29 | (BLUES, "BLUES"), 30 | (GREENS, "GREENS"), 31 | (GREYS, "GREYS"), 32 | (ORANGES, "ORANGES"), 33 | (PURPLES, "PURPLES"), 34 | (REDS, "REDS"), 35 | // Diverging 36 | (BROWN_GREEN, "BROWN_GREEN"), 37 | (PURPLE_GREEN, "PURPLE_GREEN"), 38 | (PINK_GREEN, "PINK_GREEN"), 39 | (PURPLE_ORANGE, "PURPLE_ORANGE"), 40 | (RED_BLUE, "RED_BLUE"), 41 | (RED_GREY, "RED_GREY"), 42 | (RED_YELLOW_BLUE, "RED_YELLOW_BLUE"), 43 | (RED_YELLOW_GREEN, "RED_YELLOW_GREEN"), 44 | (SPECTRAL, "SPECTRAL"), 45 | // Cyclical 46 | (RAINBOW, "RAINBOW"), 47 | (SINEBOW, "SINEBOW"), 48 | ]; 49 | 50 | const CATEGORICALS: [(&[Color], &str); 10] = [ 51 | (&CATEGORY10, "CATEGORY10"), 52 | (&ACCENT, "ACCENT"), 53 | (&DARK2, "DARK2"), 54 | (&PAIRED, "PAIRED"), 55 | (&PASTEL1, "PASTEL1"), 56 | (&PASTEL2, "PASTEL2"), 57 | (&SET1, "SET1"), 58 | (&SET2, "SET2"), 59 | (&SET3, "SET3"), 60 | (&TABLEAU10, "TABLEAU10"), 61 | ]; 62 | 63 | fn main() { 64 | let rows = GRADIENTS.len() + CATEGORICALS.len(); 65 | let margin = 2; 66 | let grid = 80; 67 | let width = 1800; 68 | let height = rows * grid - margin; 69 | let mut imgbuf = ImageBuffer::new(width as u32, height as u32); 70 | 71 | for (x, y, pixel) in imgbuf.enumerate_pixels_mut() { 72 | let (x, y) = (x as usize, y as usize); 73 | let row = y / grid; 74 | let col = x / grid; 75 | let border = y % grid >= grid - margin; 76 | *pixel = if let Some((gradient, _)) = GRADIENTS.get(row) { 77 | if border { 78 | image::Rgb([0, 0, 0]) 79 | } else { 80 | let i = x.saturating_sub(10); 81 | let n = width - 20; 82 | let Color { r, g, b } = gradient.eval_rational(i, n); 83 | image::Rgb([r, g, b]) 84 | } 85 | } else if let Some((scheme, _)) = CATEGORICALS.get(row - GRADIENTS.len()) { 86 | if col >= scheme.len() { 87 | let ch = ((x + y) / 20 % 2 * 15) as u8 + 10; 88 | image::Rgb([ch, ch, ch]) 89 | } else if border { 90 | image::Rgb([0, 0, 0]) 91 | } else { 92 | let Color { r, g, b } = scheme[col]; 93 | image::Rgb([r, g, b]) 94 | } 95 | } else { 96 | image::Rgb([0, 0, 0]) 97 | }; 98 | } 99 | 100 | let buf = std::fs::read("fonts/Arial.ttf").expect("Failed to read font file"); 101 | let font = ab_glyph::FontArc::try_from_vec(buf.to_owned()).unwrap(); 102 | 103 | for row in 0..rows { 104 | let name = if let Some((_, name)) = GRADIENTS.get(row) { 105 | name 106 | } else if let Some((_, name)) = CATEGORICALS.get(row - GRADIENTS.len()) { 107 | name 108 | } else { 109 | continue; 110 | }; 111 | imageproc::drawing::draw_text_mut( 112 | &mut imgbuf, 113 | image::Rgb([100, 100, 100]), 114 | 10, 115 | (row * grid + 10) as i32, 116 | 24.0, 117 | &font, 118 | name, 119 | ); 120 | } 121 | 122 | if let Err(err) = imgbuf.save("colorous.png") { 123 | eprintln!("Error: {}", err); 124 | process::exit(1); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /examples/decoding.rs: -------------------------------------------------------------------------------- 1 | use image::{ImageBuffer, Rgb}; 2 | 3 | use rsmedia::{filter, DecoderBuilder, MediaFrame, MediaType}; 4 | 5 | use anyhow::{Context, Result}; 6 | use futures::future::join_all; 7 | 8 | use once_cell::sync::Lazy; 9 | use std::sync::Mutex; 10 | use tokio::task; 11 | 12 | const OUTPUT_DIR: &'static str = "output"; 13 | static FRAME_COUNT: Lazy> = Lazy::new(|| Mutex::new(0)); 14 | static SAVE_TASKS: Lazy>>> = Lazy::new(|| Mutex::new(Vec::new())); 15 | 16 | #[tokio::main] 17 | async fn main() -> Result<()> { 18 | tracing_subscriber::fmt() 19 | .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) 20 | .with_timer(tracing_subscriber::fmt::time::ChronoLocal::rfc_3339()) 21 | .with_target(true) 22 | .with_file(true) 23 | .with_line_number(true) 24 | .with_thread_ids(true) 25 | .init(); 26 | 27 | rsmedia::init().unwrap(); 28 | 29 | let source = std::path::Path::new("/tmp/bear.mp4"); 30 | 31 | // 640x360 mp4 32 | // let source = "https://img.qunliao.info/4oEGX68t_9505974551.mp4" 33 | // .parse::() 34 | // .unwrap(); 35 | 36 | let filters = vec![ 37 | filter::video::scale(640, 360, None), 38 | filter::video::fps(30.0), 39 | ]; 40 | 41 | let mut decoder = DecoderBuilder::new(MediaType::VIDEO) 42 | // decoder with CUDA acceleration 43 | // .with_hardware_device(Some(HWDeviceType::CUDA.auto_best_config().unwrap())) 44 | // .with_codec_name(Some("h264_cuvid".to_string())) 45 | .with_filters(Some(filters)) 46 | .build_wrapped(source) 47 | .context("failed to create decoder")?; 48 | 49 | std::fs::create_dir_all(OUTPUT_DIR).context("failed to create output directory")?; 50 | 51 | // seek to the 20th frame 52 | decoder.seek_to_frame(20).unwrap(); 53 | 54 | loop { 55 | match decoder.decode::() { 56 | Ok(Some(yuv_frame)) => { 57 | println!( 58 | "decoded frame pts: {}, type: {:?}, format:{:?}", 59 | yuv_frame.pts, yuv_frame.media_type, yuv_frame.format 60 | ); 61 | 62 | process_frame(yuv_frame)?; 63 | } 64 | Ok(None) => { 65 | println!("Decoder has reached the end of the stream"); 66 | break; 67 | } 68 | Err(e) => { 69 | println!("Error decoding frame: {}", e); 70 | break; 71 | } 72 | } 73 | } 74 | 75 | { 76 | // Waiting for all tasks to be completed 77 | let tasks = SAVE_TASKS.lock().unwrap().drain(..).collect::>(); 78 | join_all(tasks).await; 79 | } 80 | 81 | println!( 82 | "Saved {} frames in the '{}' directory", 83 | FRAME_COUNT.lock().unwrap(), 84 | OUTPUT_DIR 85 | ); 86 | 87 | Ok(()) 88 | } 89 | 90 | fn process_frame(yuv_frame: MediaFrame) -> Result<()> { 91 | let rgb_frame = yuv_frame.convert_yuv_to_rgb()?; 92 | 93 | let img: ImageBuffer, Vec> = ImageBuffer::from_raw( 94 | yuv_frame.width as u32, 95 | yuv_frame.height as u32, 96 | rgb_frame.data.as_slice().unwrap().to_vec(), 97 | ) 98 | .context("failed to create image buffer")?; 99 | 100 | let frame_path = format!( 101 | "{}/frame_{:05}.png", 102 | OUTPUT_DIR, 103 | FRAME_COUNT.lock().unwrap() 104 | ); 105 | 106 | let task = task::spawn_blocking(move || { 107 | img.save(&frame_path).expect("failed to save frame"); 108 | }); 109 | 110 | SAVE_TASKS.lock().unwrap().push(task); 111 | 112 | *FRAME_COUNT.lock().unwrap() += 1; 113 | 114 | Ok(()) 115 | } 116 | -------------------------------------------------------------------------------- /examples/encoding.rs: -------------------------------------------------------------------------------- 1 | use rsmedia::{ 2 | colors, filter, 3 | frame::MediaFrame, 4 | time::{self, Time}, 5 | EncoderBuilder, PixelFormat, 6 | }; 7 | 8 | use std::path::Path; 9 | 10 | fn main() -> anyhow::Result<()> { 11 | tracing_subscriber::fmt() 12 | .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) 13 | .with_timer(tracing_subscriber::fmt::time::ChronoLocal::rfc_3339()) 14 | .with_target(true) 15 | .with_file(true) 16 | .with_line_number(true) 17 | .with_thread_ids(true) 18 | .init(); 19 | 20 | rsmedia::init().unwrap(); 21 | 22 | let width = 640; 23 | let height = 640; 24 | 25 | let filters = vec![ 26 | filter::video::scale(1280, 720, Some("bicubic")), 27 | filter::video::crop(20, 20, width, height), 28 | filter::video::drawtext("Watermark", 50, 50, "fonts/Arial.ttf", 18, "white@0.5"), 29 | ]; 30 | 31 | let output_path = Path::new("/tmp/rainbow.mp4"); 32 | 33 | let mut encoder = EncoderBuilder::new_video(width as usize, height as usize) 34 | // encoder with CUDA acceleration 35 | // .with_hardware_device(Some(HWDeviceType::CUDA.auto_best_config().unwrap())) 36 | // libx264, libx265, h264_nvenc, h264_vaapi 37 | // .with_codec_name(Some("h264_nvenc".to_string())) 38 | // .with_options(Some(Options::preset_h264_nvenc())) 39 | .with_filters(Some(filters)) 40 | .build_wrapped(output_path) 41 | .expect("failed to create encoder"); 42 | 43 | let duration: Time = Time::from_nth_of_a_second(24); 44 | let mut position = Time::zero(); 45 | 46 | for i in 0..256 { 47 | // This will create a smooth rainbow animation video! 48 | let mut frame = rainbow_frame(width as usize, height as usize, i as f32 / 256.0); 49 | 50 | frame.set_pts( 51 | position 52 | .aligned_with_rational(encoder.time_base()) 53 | .into_value() 54 | .unwrap(), 55 | ); 56 | 57 | encoder.encode(frame)?; 58 | 59 | println!("Encoded frame {} at position {}", i, position); 60 | 61 | // Update the current position and add the inter-frame duration to it. 62 | position = position.aligned_with(duration).add(); 63 | } 64 | 65 | encoder.finish()?; 66 | 67 | Ok(()) 68 | } 69 | 70 | fn rainbow_frame(width: usize, height: usize, p: f32) -> MediaFrame { 71 | // This is what generated the rainbow effect! 72 | // We loop through the HSV color spectrum and convert to RGB. 73 | let rgb = colors::hsv_to_rgb(p * 360.0, 100.0, 100.0); 74 | 75 | // This creates a frame with height 720, width 1280 and three channels. The RGB values for each 76 | // pixel are equal, and determined by the `rgb` we chose above. 77 | let mut frame = MediaFrame::::new_video_frame( 78 | width, 79 | height, 80 | PixelFormat::RGB24, 81 | time::new_rational(1, 24), 82 | ) 83 | .unwrap(); 84 | for y in 0..height { 85 | for x in 0..width { 86 | frame.data[[y, x, 0]] = rgb[0]; 87 | frame.data[[y, x, 1]] = rgb[1]; 88 | frame.data[[y, x, 2]] = rgb[2]; 89 | } 90 | } 91 | frame 92 | } 93 | -------------------------------------------------------------------------------- /examples/misc/av_convert.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use image::RgbImage; 3 | use rsmedia::swctx; 4 | use rsmedia::PixelFormat; 5 | use rsmpeg::{avutil::AVFrame, ffi}; 6 | 7 | /// 将 RgbImage 转换为 AVFrame 8 | pub fn image_rgb_to_avframe_rgb24(image: &RgbImage, frame_pts: i64) -> Result { 9 | let (width, height) = image.dimensions(); 10 | 11 | // 2. 创建源 AVFrame,并分配缓冲区 12 | let mut frame = AVFrame::new(); 13 | frame.set_width(width as i32); 14 | frame.set_height(height as i32); 15 | frame.set_format(ffi::AV_PIX_FMT_RGB24); 16 | frame.set_pts(frame_pts); 17 | frame.alloc_buffer().unwrap(); 18 | 19 | // 3. 将 image 的 RGB 数据拷贝到 src_frame 中 20 | // let data_arr = ndarray::Array3::from_shape_vec((height as usize, width as usize, 3), image.into_raw()) 21 | // .expect("Failed to create ndarray from raw image data"); 22 | unsafe { 23 | let rgb_data = image.as_raw(); 24 | let buffer_slice = std::slice::from_raw_parts_mut(frame.data[0], rgb_data.len()); 25 | buffer_slice.copy_from_slice(rgb_data); 26 | } 27 | 28 | Ok(frame) 29 | } 30 | 31 | /// 将 RgbImage 转换为 AVFrame 32 | pub fn image_rgb_to_avframe_yuv420p(image: &RgbImage, frame_pts: i64) -> Result { 33 | let rgb_frame = image_rgb_to_avframe_rgb24(image, frame_pts)?; 34 | swctx::scale_frame( 35 | &rgb_frame, 36 | rgb_frame.width, 37 | rgb_frame.height, 38 | PixelFormat::YUV420P, 39 | ) 40 | } 41 | 42 | /// 将 AVFrame RGB24 转换为 RgbImage 43 | /// 按行处理数据,跳过每行末尾的对齐字节,确保只有有效的像素数据被用来创建图像,因此能生成正确的 RGB 图像。 44 | pub fn avframe_rgb24_to_image_rgb(rgb_frame: &AVFrame) -> Result { 45 | // 确保 AVFrame 的格式是 RGB24 46 | if rgb_frame.format != ffi::AV_PIX_FMT_RGB24 { 47 | return Err(anyhow::anyhow!("Unsupported pixel format")); 48 | } 49 | 50 | let width = rgb_frame.width as usize; 51 | let height = rgb_frame.height as usize; 52 | let frame_data = rgb_frame.data[0]; 53 | let linesize = rgb_frame.linesize[0] as usize; 54 | 55 | // 方法一: 56 | // 存在的问题: 假设图像数据是连续的,并且 line_size == width * 3,但实际情况并非总是如此。 57 | // 如果图像有对齐字节,直接按 line_size * height 来处理会包含额外的数据,导致图像显示错误。 58 | // let buffer = unsafe { std::slice::from_raw_parts(frame_data as *const u8, linesize * height) }; 59 | 60 | // 方法二: 61 | // 按行处理数据,跳过每行末尾的对齐字节,确保只有有效的像素数据被用来创建图像,因此能生成正确的 RGB 图像 62 | let mut buffer: Vec = Vec::with_capacity(width * height * 3); 63 | // 逐行读取 AVFrame 的数据,确保正确处理每行的 linesize 64 | for y in 0..height { 65 | let offset = y * linesize; 66 | let src = unsafe { std::slice::from_raw_parts(frame_data.add(offset), width * 3) }; 67 | buffer.extend_from_slice(src); 68 | } 69 | 70 | // 使用 buffer 数据创建 RgbImage 71 | // 第一种方式(RgbImage) 更简洁、明确,并且适用于绝大多数场景,因为它将通道类型和缓冲区类型都固定为常见的组合(Rgb 和 Vec)。 72 | let rgb_image = RgbImage::from_raw(width as u32, height as u32, buffer) 73 | .ok_or_else(|| "Failed to create RgbImage") 74 | .unwrap(); 75 | 76 | // 第二种方式(ImageBuffer, _>) 更加通用。你可以使用不同类型的缓冲区(如 &[u8]、Box<[u8]> 等), 77 | // 而不仅仅是 Vec。它为你提供了更大的灵活性,但也稍微冗长。 78 | // let image_buffer: ImageBuffer, _> = ImageBuffer::from_raw(width as u32, height as u32, buffer) 79 | // .ok_or_else(|| "Failed to create image buffer").unwrap(); 80 | 81 | Ok(rgb_image) 82 | } 83 | 84 | /// 将 AVFrame YUV420P 转换为 RgbImage 85 | pub fn avframe_yuv420p_to_image_rgb(frame: &AVFrame) -> Result { 86 | let rgb_frame = swctx::scale_frame(&frame, frame.width, frame.height, PixelFormat::RGB24)?; 87 | avframe_rgb24_to_image_rgb(&rgb_frame) 88 | } 89 | 90 | #[cfg(test)] 91 | mod tests { 92 | use super::*; 93 | use anyhow::Context; 94 | 95 | /// create RgbImage 96 | fn create_rgb_image() -> RgbImage { 97 | let width = 320; 98 | let height = 240; 99 | let mut img = RgbImage::new(width, height); 100 | 101 | // 创建一个简单的渐变图案 102 | for y in 0..height { 103 | for x in 0..width { 104 | let r = (x as f32 / width as f32 * 255.0) as u8; 105 | let g = (y as f32 / height as f32 * 255.0) as u8; 106 | let b = ((x + y) as f32 / (width + height) as f32 * 255.0) as u8; 107 | img.put_pixel(x, y, image::Rgb([r, g, b])); 108 | } 109 | } 110 | img 111 | } 112 | 113 | /// create AVFrame 114 | fn create_yuv_avframe() -> Result { 115 | let width = 320; 116 | let height = 240; 117 | let pixel_format = ffi::AV_PIX_FMT_YUV420P; 118 | 119 | let mut yuv_frame = AVFrame::new(); 120 | yuv_frame.set_width(width); 121 | yuv_frame.set_height(height); 122 | yuv_frame.set_format(pixel_format); 123 | yuv_frame 124 | .alloc_buffer() 125 | .context("frame alloc_buffer failed, error.") 126 | .unwrap(); 127 | 128 | Ok(yuv_frame) 129 | } 130 | 131 | #[test] 132 | fn test_avframe_to_image() -> Result<()> { 133 | let yuv_frame = create_yuv_avframe()?; 134 | 135 | let img = avframe_yuv420p_to_image_rgb(&yuv_frame)?; 136 | 137 | assert_eq!(img.width(), yuv_frame.width as u32); 138 | assert_eq!(img.height(), yuv_frame.height as u32); 139 | 140 | img.save("/tmp/test_avframe_to_image.png") 141 | .expect("avframe_to_image error"); 142 | 143 | Ok(()) 144 | } 145 | 146 | #[test] 147 | fn test_image_to_avframe() -> Result<()> { 148 | let rgb_img = create_rgb_image(); 149 | 150 | let frame = image_rgb_to_avframe_rgb24(&rgb_img, 0)?; 151 | 152 | assert_eq!(frame.width as u32, rgb_img.width()); 153 | assert_eq!(frame.height as u32, rgb_img.height()); 154 | assert_eq!(frame.format, ffi::AV_PIX_FMT_RGB24 as i32); 155 | println!( 156 | "frame.width: {}, frame.height: {}, frame.format: {}", 157 | frame.width, frame.height, frame.format 158 | ); 159 | 160 | Ok(()) 161 | } 162 | 163 | #[test] 164 | fn test_roundtrip_conversions() -> Result<()> { 165 | // Test image -> AVFrame -> image 166 | let original_img = create_rgb_image(); 167 | 168 | let rgb_frame = image_rgb_to_avframe_rgb24(&original_img, 0)?; 169 | let converted_img = avframe_rgb24_to_image_rgb(&rgb_frame)?; 170 | assert_eq!(original_img.dimensions(), converted_img.dimensions()); 171 | 172 | // Test image -> Mat -> image 173 | // let mat = image_to_mat(&original_img)?; 174 | // let converted_img2 = mat_to_image(&mat)?; 175 | // assert_eq!(original_img.dimensions(), converted_img2.dimensions()); 176 | 177 | Ok(()) 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /examples/misc/av_spliter.rs: -------------------------------------------------------------------------------- 1 | //! A test that split a video(H.264, AAC) file to a AAC file and a H.264 2 | //! file(Annex B). Showing the usage of `AVBitStream` related APIs. 3 | use anyhow::{Context, Result}; 4 | use rsmpeg::{ 5 | avcodec::{AVBSFContextUninit, AVBitStreamFilter}, 6 | avformat::{AVFormatContextInput, AVFormatContextOutput}, 7 | error::RsmpegError, 8 | }; 9 | use std::{ffi::CStr, fs::File, io::Write}; 10 | 11 | fn av_spliter(file_path: &CStr, out_video: &str, out_audio: &CStr) -> Result<()> { 12 | let mut out_video = File::create(out_video)?; 13 | 14 | let mut input_format_context = AVFormatContextInput::open(file_path, None, &mut None)?; 15 | input_format_context.dump(0, file_path)?; 16 | 17 | let video_index = input_format_context 18 | .streams() 19 | .into_iter() 20 | .position(|x| x.codecpar().codec_type().is_video()) 21 | .context("Cannot find video stream!")?; 22 | let audio_index = input_format_context 23 | .streams() 24 | .into_iter() 25 | .position(|x| x.codecpar().codec_type().is_audio()) 26 | .context("Cannot find audio stream!")?; 27 | 28 | let bsf = AVBitStreamFilter::find_by_name(c"h264_mp4toannexb") 29 | .context("Failed to find bit stream filter")?; 30 | 31 | let mut bsf_context = { 32 | let mut bsf_context = AVBSFContextUninit::new(&bsf); 33 | let video_stream = &input_format_context.streams()[video_index]; 34 | bsf_context.set_par_in(&video_stream.codecpar()); 35 | bsf_context.set_time_base_in(video_stream.time_base); 36 | bsf_context.init()? 37 | }; 38 | 39 | let mut out_audio_format_context = AVFormatContextOutput::create(out_audio, None)?; 40 | { 41 | let mut new_audio_stream = out_audio_format_context.new_stream(); 42 | let audio_stream = &input_format_context.streams()[audio_index]; 43 | new_audio_stream.set_codecpar(audio_stream.codecpar().clone()); 44 | new_audio_stream.set_time_base(audio_stream.time_base); 45 | } 46 | out_audio_format_context.write_header(&mut None)?; 47 | 48 | while let Some(mut packet) = input_format_context.read_packet()? { 49 | let packet_stream_index = packet.stream_index as usize; 50 | if packet_stream_index == video_index { 51 | bsf_context.send_packet(Some(&mut packet))?; 52 | loop { 53 | match bsf_context.receive_packet(&mut packet) { 54 | Ok(()) => { 55 | let data = unsafe { 56 | std::slice::from_raw_parts(packet.data, packet.size as usize) 57 | }; 58 | out_video.write_all(data)?; 59 | } 60 | Err(RsmpegError::BitstreamDrainError) 61 | | Err(RsmpegError::BitstreamFlushedError) => break, 62 | Err(e) => anyhow::bail!(e), 63 | } 64 | } 65 | } else if packet_stream_index == audio_index { 66 | packet.set_stream_index(0); 67 | out_audio_format_context.write_frame(&mut packet)?; 68 | } 69 | } 70 | 71 | out_audio_format_context.write_trailer()?; 72 | 73 | out_video.flush()?; 74 | 75 | Ok(()) 76 | } 77 | 78 | #[cfg(test)] 79 | mod tests { 80 | use super::av_spliter; 81 | 82 | #[test] 83 | #[ignore = "test_av_spliter0 测试运行依赖测试文件,暂时忽略"] 84 | fn test_av_spliter0() { 85 | std::fs::create_dir_all("tests/output/av_spliter").unwrap(); 86 | av_spliter( 87 | c"tests/assets/vids/bunny.flv", 88 | "tests/output/av_spliter/out_video_bunny.h264", 89 | c"tests/output/av_spliter/out_audio_bunny.aac", 90 | ) 91 | .unwrap(); 92 | } 93 | 94 | #[test] 95 | #[ignore = "test_av_spliter1 测试运行依赖测试文件,暂时忽略"] 96 | fn test_av_spliter1() { 97 | std::fs::create_dir_all("tests/output/av_spliter").unwrap(); 98 | av_spliter( 99 | c"tests/assets/vids/bear.mp4", 100 | "tests/output/av_spliter/out_video_bear.h264", 101 | c"tests/output/av_spliter/out_audio_bear.aac", 102 | ) 103 | .unwrap(); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /examples/misc/avio_reading.rs: -------------------------------------------------------------------------------- 1 | //! Demo of custom IO using `AVIOContextCustom`. 2 | use super::avio; 3 | use anyhow::{Context, Result}; 4 | use rsmpeg::ffi; 5 | use rsmpeg::{ 6 | avformat::{AVFormatContextInput, AVIOContextContainer, AVIOContextCustom}, 7 | avutil::{AVMem, AVMmap}, 8 | error::RsmpegError, 9 | }; 10 | use std::ffi::CStr; 11 | use std::sync::atomic::{self, AtomicI32}; 12 | 13 | pub fn avio_reading(file_path: &CStr) -> Result<()> { 14 | let (video_stream_index, mut input_format_context, mut decode_context) = 15 | avio::open_input_file(file_path)?; 16 | 17 | loop { 18 | let packet = match input_format_context.read_packet() { 19 | Ok(Some(x)) => x, 20 | // No more frames 21 | Ok(None) => break, 22 | Err(e) => panic!("Read frame error: {:?}", e), 23 | }; 24 | 25 | if packet.stream_index as usize != video_stream_index { 26 | continue; 27 | } 28 | 29 | decode_context 30 | .send_packet(Some(&packet)) 31 | .context("Send packet failed")?; 32 | 33 | let frame_index = AtomicI32::new(0); 34 | loop { 35 | let mut frame = match decode_context.receive_frame() { 36 | Ok(frame) => frame, 37 | Err(RsmpegError::DecoderDrainError) | Err(RsmpegError::DecoderFlushedError) => { 38 | break; 39 | } 40 | Err(e) => panic!("{}", e), 41 | }; 42 | 43 | frame.set_pts(frame.best_effort_timestamp); 44 | 45 | // avio::pgm_save(&frame, &format!("{}/frame_{}.pgm", "/tmp", frame_index.fetch_add(1, atomic::Ordering::SeqCst)))?; 46 | avio::save_avframe_to_image( 47 | &frame, 48 | &format!( 49 | "{}/frame_{}.jpeg", 50 | "/tmp", 51 | frame_index.fetch_add(1, atomic::Ordering::SeqCst) 52 | ), 53 | )?; 54 | } 55 | } 56 | 57 | Ok(()) 58 | } 59 | 60 | fn avio_file_reading(filename: &CStr) -> Result<()> { 61 | let mmap = AVMmap::new(filename)?; 62 | let mut current = 0; 63 | 64 | let io_context = AVIOContextCustom::alloc_context( 65 | AVMem::new(4096), 66 | false, 67 | vec![], 68 | Some(Box::new(move |_, buf| { 69 | let right = mmap.len().min(current + buf.len()); 70 | if right <= current { 71 | return ffi::AVERROR_EOF; 72 | } 73 | let read_len = right - current; 74 | buf[0..read_len].copy_from_slice(&mmap[current..right]); 75 | current = right; 76 | read_len as i32 77 | })), 78 | None, 79 | None, 80 | ); 81 | 82 | let mut input_format_context = 83 | AVFormatContextInput::from_io_context(AVIOContextContainer::Custom(io_context))?; 84 | input_format_context.dump(0, filename)?; 85 | 86 | Ok(()) 87 | } 88 | 89 | #[cfg(test)] 90 | mod tests { 91 | use super::*; 92 | 93 | #[test] 94 | #[ignore = "test_avio_reading0 测试运行依赖测试文件,暂时忽略"] 95 | fn test_avio_reading0() { 96 | avio_file_reading(c"tests/assets/vids/bear.mp4").unwrap(); 97 | } 98 | 99 | #[test] 100 | #[ignore = "test_avio_reading1 测试运行依赖测试文件,暂时忽略"] 101 | fn test_avio_reading1() { 102 | avio_file_reading(c"tests/assets/vids/centaur.mpg").unwrap(); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /examples/misc/avio_writing.rs: -------------------------------------------------------------------------------- 1 | use super::avio; 2 | /// Simplified transcoding test, select the first video stream in given video file 3 | /// and transcode it. Store the output in memory. 4 | use anyhow::{bail, Context, Result}; 5 | use rsmpeg::ffi; 6 | use rsmpeg::{ 7 | avcodec::AVCodecContext, avformat::AVFormatContextOutput, avutil::AVFrame, error::RsmpegError, 8 | }; 9 | use std::ffi::CStr; 10 | 11 | /// encode -> write_frame 12 | pub fn encode_write_frame( 13 | frame_after: Option<&AVFrame>, 14 | encode_context: &mut AVCodecContext, 15 | output_format_context: &mut AVFormatContextOutput, 16 | out_stream_index: usize, 17 | ) -> Result<()> { 18 | encode_context 19 | .send_frame(frame_after) 20 | .context("Encode frame failed.")?; 21 | 22 | loop { 23 | let mut packet = match encode_context.receive_packet() { 24 | Ok(packet) => packet, 25 | Err(RsmpegError::EncoderDrainError) | Err(RsmpegError::EncoderFlushedError) => break, 26 | Err(e) => bail!(e), 27 | }; 28 | 29 | packet.set_stream_index(out_stream_index as i32); 30 | packet.rescale_ts( 31 | encode_context.time_base, 32 | output_format_context.streams()[out_stream_index].time_base, 33 | ); 34 | 35 | match output_format_context.interleaved_write_frame(&mut packet) { 36 | Ok(()) => Ok(()), 37 | Err(RsmpegError::AVError(-22)) => Ok(()), 38 | Err(e) => Err(e), 39 | } 40 | .context("Interleaved write frame failed.")?; 41 | } 42 | 43 | Ok(()) 44 | } 45 | 46 | /// Send an empty packet to the `encode_context` for packet flushing. 47 | pub fn flush_encoder( 48 | encode_context: &mut AVCodecContext, 49 | output_format_context: &mut AVFormatContextOutput, 50 | out_stream_index: usize, 51 | ) -> Result<()> { 52 | // 确定编码器是否支持延迟(delay) 53 | // 如果编码器不支持延迟,那么就没有必要进行 flush 操作,因为在这种情况下,编码器不会保留任何未处理的数据。 54 | // 如果编码器支持延迟(delay),则在结束编码之前发送 EOS 包是有必要的,因为编码器可能还在缓冲一些数据,直到接收到 EOS 信号才会处理完这些数据并输出剩余的包。 55 | if encode_context.codec().capabilities & ffi::AV_CODEC_CAP_DELAY as i32 == 0 { 56 | return Ok(()); 57 | } 58 | 59 | encode_write_frame( 60 | None, 61 | encode_context, 62 | output_format_context, 63 | out_stream_index, 64 | )?; 65 | Ok(()) 66 | } 67 | 68 | /// Transcoding audio and video stream in a multi media file. 69 | pub fn transcoding(input_file: &CStr, output_file: &CStr) -> Result<()> { 70 | let (video_stream_index, mut input_format_context, mut decode_context) = 71 | avio::open_input_file(input_file)?; 72 | let (mut output_format_context, mut encode_context) = 73 | avio::open_output_file(output_file, &decode_context)?; 74 | 75 | loop { 76 | let mut packet = match input_format_context.read_packet() { 77 | Ok(Some(x)) => x, 78 | // No more frames 79 | Ok(None) => break, 80 | Err(e) => bail!("Read frame error: {:?}", e), 81 | }; 82 | 83 | if packet.stream_index as usize != video_stream_index { 84 | continue; 85 | } 86 | 87 | packet.rescale_ts( 88 | input_format_context.streams()[video_stream_index].time_base, 89 | encode_context.time_base, 90 | ); 91 | 92 | decode_context 93 | .send_packet(Some(&packet)) 94 | .context("Send packet failed")?; 95 | 96 | loop { 97 | let mut frame = match decode_context.receive_frame() { 98 | Ok(frame) => frame, 99 | Err(RsmpegError::DecoderDrainError) | Err(RsmpegError::DecoderFlushedError) => { 100 | break; 101 | } 102 | Err(e) => bail!(e), 103 | }; 104 | 105 | frame.set_pts(frame.best_effort_timestamp); 106 | encode_write_frame( 107 | Some(&frame), 108 | &mut encode_context, 109 | &mut output_format_context, 110 | 0, 111 | )?; 112 | } 113 | } 114 | 115 | // Flush the encoder by pushing EOF frame to encode_context. 116 | flush_encoder(&mut encode_context, &mut output_format_context, 0)?; 117 | output_format_context.write_trailer()?; 118 | Ok(()) 119 | } 120 | 121 | pub fn clip_video( 122 | input_file: &CStr, 123 | output_file: &CStr, 124 | start_time: f64, 125 | duration: f64, 126 | ) -> Result<()> { 127 | let (video_stream_index, mut input_format_context, mut decode_context) = 128 | avio::open_input_file(input_file)?; 129 | 130 | let (mut output_format_context, mut encode_context) = 131 | avio::open_output_file(output_file, &decode_context)?; 132 | 133 | let video_stream = &input_format_context.streams()[video_stream_index]; 134 | let start_time_base = video_stream.time_base; 135 | // Convert c_int to f64 for division 136 | let (num, den) = (start_time_base.num as f64, start_time_base.den as f64); 137 | 138 | let start_time_ticks = start_time * (den / num); 139 | let duration_ticks = duration * (den / num); 140 | 141 | println!( 142 | "time_base: {:?}, start_time_ticks: {:?}, duration_ticks: {:?}", 143 | start_time_base, start_time_ticks, duration_ticks 144 | ); 145 | 146 | let mut current_time_ticks = 0; 147 | 148 | while let Ok(Some(mut packet)) = input_format_context.read_packet() { 149 | if packet.stream_index as usize != video_stream_index { 150 | continue; 151 | } 152 | 153 | packet.rescale_ts( 154 | input_format_context.streams()[video_stream_index].time_base, 155 | encode_context.time_base, 156 | ); 157 | 158 | println!( 159 | "packet.pts: {:?}, packet.dts: {:?}, current_time_ticks: {:?}", 160 | packet.pts, packet.dts, current_time_ticks 161 | ); 162 | 163 | // Check if we should skip this packet based on time 164 | // PTS(Presentation Time Stamp) 165 | // 定义:pts 指定了帧应该显示的时间点。 166 | // 用途:通常用于同步不同流(比如视频和音频)的播放时间。播放器会按照这个时间戳来展示帧。 167 | // 重要性:对于视频和音频的同步至关重要。 168 | // DTS(Decoding Time Stamp) 169 | // 定义:dts 指定了帧应该解码的时间点。 170 | // 用途:通常用于解码器知道何时开始解码一个帧。一些解码器需要知道这个信息来正确处理依赖关系。 171 | // 重要性:对于解码顺序有影响,特别是对于那些具有 B 帧(B-frame)依赖性的视频编码器来说。 172 | // 区别: 173 | // 顺序:在某些情况下,dts 可能比 pts 小,这是因为有些帧需要在逻辑上先解码但后显示(例如 B 帧)。 174 | // 存在性:并非所有的媒体格式或编码都会同时使用这两个时间戳。有时一个或另一个可能不存在或未定义。 175 | // 使用场景: 176 | // 解码:当解码器需要知道解码顺序时,使用 dts。 177 | // 呈现:当需要知道显示顺序时,使用 pts。 178 | // 如何选择使用 pts 或 dts : 179 | // 在大多数情况下,如果 pts 存在,那么优先使用 pts,因为它更准确地反映了帧应该显示的时间点。如果没有 pts,则可以使用 dts。 180 | let packet_time = if packet.pts > 0 { 181 | packet.pts as f64 182 | } else { 183 | packet.dts as f64 184 | }; 185 | if packet_time < start_time_ticks { 186 | continue; 187 | } 188 | 189 | // Check if we have reached the end time 190 | if current_time_ticks as f64 >= start_time_ticks + duration_ticks { 191 | break; 192 | } 193 | 194 | decode_context 195 | .send_packet(Some(&packet)) 196 | .context("Send packet failed")?; 197 | 198 | while let Ok(mut frame) = decode_context.receive_frame() { 199 | frame.set_pts(frame.best_effort_timestamp); 200 | current_time_ticks = frame.pts; 201 | 202 | if current_time_ticks >= start_time_ticks as i64 { 203 | encode_write_frame( 204 | Some(&frame), 205 | &mut encode_context, 206 | &mut output_format_context, 207 | 0, 208 | )?; 209 | } 210 | } 211 | } 212 | 213 | // Flush the encoder by pushing EOS packet to encode_context. 214 | flush_encoder(&mut encode_context, &mut output_format_context, 0)?; 215 | output_format_context.write_trailer()?; 216 | Ok(()) 217 | } 218 | 219 | #[cfg(test)] 220 | mod tests { 221 | use super::*; 222 | 223 | #[test] 224 | #[ignore = "avio_writing_test0 测试运行依赖测试文件,暂时忽略"] 225 | fn avio_writing_test0() { 226 | std::fs::create_dir_all("tests/output/avio_writing/").unwrap(); 227 | transcoding( 228 | c"tests/assets/vids/mov_sample.mov", 229 | c"tests/output/avio_writing/mov_sample.mp4", 230 | ) 231 | .unwrap(); 232 | } 233 | 234 | #[test] 235 | #[ignore = "avio_writing_test1 测试运行依赖测试文件,暂时忽略"] 236 | fn avio_writing_test1() { 237 | std::fs::create_dir_all("tests/output/avio_writing/").unwrap(); 238 | transcoding( 239 | c"tests/assets/vids/centaur.mpg", 240 | c"tests/output/avio_writing/centaur.mp4", 241 | ) 242 | .unwrap(); 243 | } 244 | 245 | #[test] 246 | #[ignore = "avio_writing_test2 测试运行依赖测试文件,暂时忽略"] 247 | fn avio_writing_test2() { 248 | std::fs::create_dir_all("tests/output/avio_writing/").unwrap(); 249 | transcoding( 250 | c"tests/assets/vids/bear.mp4", 251 | c"tests/output/avio_writing/bear.mp4", 252 | ) 253 | .unwrap(); 254 | } 255 | 256 | #[test] 257 | #[ignore = "avio_writing_test3 测试运行依赖测试文件,暂时忽略"] 258 | fn avio_writing_test3() { 259 | std::fs::create_dir_all("tests/output/avio_writing/").unwrap(); 260 | transcoding( 261 | c"tests/assets/vids/vp8.mp4", 262 | c"tests/output/avio_writing/vp8.mp4", 263 | ) 264 | .unwrap(); 265 | } 266 | 267 | #[test] 268 | #[ignore = "avio_writing_test4 测试运行依赖测试文件,暂时忽略"] 269 | fn avio_writing_test4() { 270 | std::fs::create_dir_all("tests/output/avio_writing/").unwrap(); 271 | transcoding( 272 | c"tests/assets/vids/big_buck_bunny.mp4", 273 | c"tests/output/avio_writing/big_buck_bunny.mp4", 274 | ) 275 | .unwrap(); 276 | } 277 | 278 | #[test] 279 | #[ignore = "avio_writing_test5 测试运行依赖测试文件,暂时忽略"] 280 | fn avio_writing_test5() { 281 | std::fs::create_dir_all("tests/output/avio_writing/").unwrap(); 282 | transcoding( 283 | c"tests/assets/vids/with_pic.mp4", 284 | c"tests/output/avio_writing/with_pic.mp4", 285 | ) 286 | .unwrap(); 287 | } 288 | 289 | #[test] 290 | #[ignore = "avio_writing_test6 测试运行依赖测试文件,暂时忽略"] 291 | fn avio_writing_test6() { 292 | std::fs::create_dir_all("tests/output/avio_writing/").unwrap(); 293 | clip_video( 294 | c"http://172.24.82.2/video/final_134_raw.mp4", 295 | c"tests/output/avio_writing/clip_video.mp4", 296 | 0.0, 297 | 1.0, 298 | ) 299 | .unwrap(); 300 | } 301 | } 302 | -------------------------------------------------------------------------------- /examples/misc/image_dump.rs: -------------------------------------------------------------------------------- 1 | use rsmpeg::avformat::*; 2 | use std::ffi::CStr; 3 | 4 | /// Dump video/audio/image info to stdout. 5 | fn image_dump(image_path: &CStr) -> Result<(), Box> { 6 | let mut input_format_context = AVFormatContextInput::open(image_path, None, &mut None)?; 7 | input_format_context.dump(0, image_path)?; 8 | Ok(()) 9 | } 10 | 11 | #[cfg(test)] 12 | mod tests { 13 | use super::*; 14 | 15 | #[test] 16 | #[ignore = "image_dump_test 测试运行依赖测试文件,暂时忽略"] 17 | fn image_dump_test() { 18 | image_dump(c"tests/assets/pics/bear.jpg").unwrap(); 19 | image_dump(c"tests/assets/pics/gif.webp").unwrap(); 20 | image_dump(c"tests/assets/pics/mail.jpg").unwrap(); 21 | image_dump(c"tests/assets/pics/mountain.jpg").unwrap(); 22 | image_dump(c"tests/assets/pics/pink.jpg").unwrap(); 23 | image_dump(c"tests/assets/pics/redwine.jpg").unwrap(); 24 | image_dump(c"tests/assets/pics/sea.jpg").unwrap(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/misc/metadata.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | use rsmpeg::{avcodec::AVCodecContext, avformat::AVFormatContextInput, avutil, ffi}; 3 | use std::ffi::CString; 4 | 5 | /// Get metadata key-value pair form a video file. 6 | pub fn metadata(file: &str) -> Result> { 7 | let mut result = vec![]; 8 | result.push(("file_path".into(), file.to_string())); 9 | 10 | let file = CString::new(file).unwrap(); 11 | let input_format_context = AVFormatContextInput::open(&file, None, &mut None).unwrap(); 12 | 13 | // Get `duration` and `bit_rate` from `input_format_context`. 14 | result.push(("duration".into(), input_format_context.duration.to_string())); 15 | result.push(("bit_rate".into(), input_format_context.bit_rate.to_string())); 16 | 17 | // Get additional info from `input_format_context.metadata()` 18 | if let Some(metadata) = input_format_context.metadata() { 19 | for entry in metadata.iter() { 20 | result.push(( 21 | entry.key().to_str().unwrap().to_string(), 22 | entry.value().to_str().unwrap().to_string(), 23 | )); 24 | } 25 | } 26 | 27 | { 28 | // Get `frame_rate` from `video_stream` 29 | let (video_stream_index, decoder) = input_format_context 30 | .find_best_stream(ffi::AVMEDIA_TYPE_VIDEO)? 31 | .context("Failed to find video stream")?; 32 | 33 | let video_stream = &input_format_context.streams()[video_stream_index]; 34 | 35 | result.push(( 36 | "frame_rate".into(), 37 | avutil::av_q2d(video_stream.r_frame_rate).to_string(), 38 | )); 39 | 40 | // Get `width` and `height` from `decode_context` 41 | let mut decode_context = AVCodecContext::new(&decoder); 42 | decode_context 43 | .apply_codecpar(&video_stream.codecpar()) 44 | .unwrap(); 45 | decode_context.open(None).unwrap(); 46 | result.push(("width".into(), decode_context.width.to_string())); 47 | result.push(("height".into(), decode_context.height.to_string())); 48 | }; 49 | 50 | Ok(result) 51 | } 52 | 53 | #[cfg(test)] 54 | mod tests { 55 | use super::metadata; 56 | 57 | #[test] 58 | #[ignore = "metadata_test0 测试运行依赖测试文件,暂时忽略"] 59 | fn metadata_test0() { 60 | assert_eq!( 61 | metadata("tests/assets/vids/bear.mp4").unwrap(), 62 | vec![ 63 | ("image_path".into(), "tests/assets/vids/bear.mp4".into()), 64 | ("duration".into(), "1068118".into()), 65 | ("bit_rate".into(), "307823".into()), 66 | ("major_brand".into(), "isom".into()), 67 | ("minor_version".into(), "1".into()), 68 | ("compatible_brands".into(), "isomavc1".into()), 69 | ("creation_time".into(), "2009-07-09T17:29:47.000000Z".into()), 70 | ("frame_rate".into(), "29.97002997002997".into()), 71 | ("width".into(), "320".into()), 72 | ("height".into(), "180".into()), 73 | ] 74 | ); 75 | } 76 | 77 | #[test] 78 | #[ignore = "metadata_test1 测试运行依赖测试文件,暂时忽略"] 79 | fn metadata_test1() { 80 | assert_eq!( 81 | metadata("tests/assets/vids/vp8.mp4").unwrap(), 82 | vec![ 83 | ("image_path".into(), "tests/assets/vids/vp8.mp4".into()), 84 | ("duration".into(), "17600000".into()), 85 | ("bit_rate".into(), "242823".into()), 86 | ("encoder".into(), "whammy".into()), 87 | ("frame_rate".into(), "5".into()), 88 | ("width".into(), "604".into()), 89 | ("height".into(), "604".into()), 90 | ] 91 | ); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /examples/misc/mod.rs: -------------------------------------------------------------------------------- 1 | mod av_convert; 2 | mod av_spliter; 3 | mod avio; 4 | mod avio_reading; 5 | mod avio_writing; 6 | mod image_dump; 7 | mod metadata; 8 | mod thumbnail; 9 | mod tutorial01; 10 | -------------------------------------------------------------------------------- /examples/misc/thumbnail.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{bail, Context, Result}; 2 | use rsmpeg::ffi; 3 | use rsmpeg::{ 4 | avcodec::{AVCodec, AVCodecContext}, 5 | avformat::AVFormatContextInput, 6 | avutil::{self, AVFrameWithImage, AVImage}, 7 | error::RsmpegError, 8 | swscale::SwsContext, 9 | }; 10 | use std::{ffi::CStr, fs::File, io::prelude::*, ops::Deref, slice}; 11 | 12 | fn thumbnail( 13 | input_video_path: &CStr, 14 | output_image_path: &CStr, 15 | width: Option, 16 | height: Option, 17 | ) -> Result<()> { 18 | let mut input_format_context = AVFormatContextInput::open(input_video_path, None, &mut None)?; 19 | 20 | let (video_stream_index, mut decode_context) = { 21 | let (stream_index, decoder) = input_format_context 22 | .find_best_stream(ffi::AVMEDIA_TYPE_VIDEO)? 23 | .context("Failed to find the best stream")?; 24 | 25 | let stream = &input_format_context.streams()[stream_index]; 26 | 27 | let mut decode_context = AVCodecContext::new(&decoder); 28 | decode_context.apply_codecpar(&stream.codecpar())?; 29 | decode_context.open(None)?; 30 | 31 | (stream_index, decode_context) 32 | }; 33 | 34 | let cover_frame = loop { 35 | let cover_packet = loop { 36 | match input_format_context.read_packet()? { 37 | Some(x) if x.stream_index != video_stream_index as i32 => {} 38 | x => break x, 39 | } 40 | }; 41 | 42 | decode_context.send_packet(cover_packet.as_ref())?; 43 | // repeatedly send packet until a frame can be extracted 44 | match decode_context.receive_frame() { 45 | Ok(x) => break x, 46 | Err(RsmpegError::DecoderDrainError) => {} 47 | Err(e) => bail!(e), 48 | } 49 | 50 | if cover_packet.is_none() { 51 | bail!("Can't find video cover frame"); 52 | } 53 | }; 54 | 55 | println!("Cover frame info: {:#?}", cover_frame); 56 | 57 | let mut encode_context = { 58 | let encoder = AVCodec::find_encoder(ffi::AV_CODEC_ID_MJPEG).context("Encoder not found")?; 59 | let mut encode_context = AVCodecContext::new(&encoder); 60 | 61 | encode_context.set_bit_rate(decode_context.bit_rate); 62 | encode_context.set_width(width.unwrap_or(decode_context.width)); 63 | encode_context.set_height(height.unwrap_or(decode_context.height)); 64 | encode_context.set_time_base(avutil::av_inv_q(decode_context.framerate)); 65 | encode_context.set_pix_fmt(if let Some(pix_fmts) = encoder.pix_fmts() { 66 | pix_fmts[0] 67 | } else { 68 | decode_context.pix_fmt 69 | }); 70 | encode_context.open(None)?; 71 | 72 | encode_context 73 | }; 74 | 75 | let scaled_cover_packet = { 76 | let mut sws_context = SwsContext::get_context( 77 | decode_context.width, 78 | decode_context.height, 79 | decode_context.pix_fmt, 80 | encode_context.width, 81 | encode_context.height, 82 | encode_context.pix_fmt, 83 | ffi::SWS_FAST_BILINEAR | ffi::SWS_PRINT_INFO, 84 | None, 85 | None, 86 | None, 87 | ) 88 | .context("Invalid swscontext parameter.")?; 89 | 90 | let image_buffer = AVImage::new( 91 | encode_context.pix_fmt, 92 | encode_context.width, 93 | encode_context.height, 94 | 1, 95 | ) 96 | .context("Image buffer parameter invalid.")?; 97 | 98 | let mut scaled_cover_frame = AVFrameWithImage::new(image_buffer); 99 | 100 | sws_context.scale_frame( 101 | &cover_frame, 102 | 0, 103 | decode_context.height, 104 | &mut scaled_cover_frame, 105 | )?; 106 | 107 | println!("{:#?}", scaled_cover_frame.deref()); 108 | 109 | encode_context.send_frame(Some(&scaled_cover_frame))?; 110 | encode_context.receive_packet()? 111 | }; 112 | 113 | let mut file = File::create(output_image_path.to_str().unwrap()).unwrap(); 114 | let data = unsafe { 115 | slice::from_raw_parts(scaled_cover_packet.data, scaled_cover_packet.size as usize) 116 | }; 117 | file.write_all(data)?; 118 | 119 | Ok(()) 120 | } 121 | 122 | #[cfg(test)] 123 | mod tests { 124 | use super::*; 125 | 126 | #[test] 127 | #[ignore = "thumbnail_test0 测试运行依赖测试文件,暂时忽略"] 128 | fn thumbnail_test0() { 129 | std::fs::create_dir_all("tests/output/thumbnail").unwrap(); 130 | 131 | thumbnail( 132 | c"tests/assets/vids/bear.mp4", 133 | c"tests/output/thumbnail/bear.jpg", 134 | Some(192), 135 | Some(108), 136 | ) 137 | .unwrap(); 138 | } 139 | 140 | #[test] 141 | #[ignore = "thumbnail_test1 测试运行依赖测试文件,暂时忽略"] 142 | fn thumbnail_test1() { 143 | std::fs::create_dir_all("tests/output/thumbnail").unwrap(); 144 | 145 | thumbnail( 146 | c"tests/assets/vids/video.mp4", 147 | c"tests/output/thumbnail/test1_video.jpg", 148 | Some(280), 149 | Some(240), 150 | ) 151 | .unwrap(); 152 | } 153 | 154 | #[test] 155 | #[ignore = "thumbnail_test2 测试运行依赖测试文件,暂时忽略"] 156 | fn thumbnail_test2() { 157 | std::fs::create_dir_all("tests/output/thumbnail").unwrap(); 158 | 159 | thumbnail( 160 | c"http://172.24.82.2/video/final_134_raw.mp4", 161 | c"tests/output/thumbnail/test2_video.jpg", 162 | Some(900), 163 | Some(600), 164 | ) 165 | .unwrap(); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /examples/misc/tutorial01.rs: -------------------------------------------------------------------------------- 1 | //! Ported from http://dranger.com/ffmpeg/tutorial01.c 2 | //! 3 | //! Extracts the first five rgb frames from the video and save them as ppm 4 | //! files. 5 | 6 | use super::avio; 7 | use anyhow::{Context, Result}; 8 | use rsmpeg::ffi; 9 | use rsmpeg::{ 10 | avcodec::{AVCodec, AVCodecContext}, 11 | avformat::AVFormatContextInput, 12 | avutil::{AVFrameWithImage, AVImage}, 13 | error::RsmpegError, 14 | swscale::SwsContext, 15 | }; 16 | use std::{ffi::CStr, fs}; 17 | 18 | fn dump_frame(file: &CStr, out_dir: &str) -> Result<()> { 19 | fs::create_dir_all(out_dir)?; 20 | let mut input_format_context = AVFormatContextInput::open(file, None, &mut None)?; 21 | input_format_context.dump(0, file)?; 22 | 23 | let video_stream_index = input_format_context 24 | .streams() 25 | .into_iter() 26 | .position(|stream| stream.codecpar().codec_type().is_video()) 27 | .context("No video stream")?; 28 | 29 | let mut decode_context = { 30 | let video_stream = &input_format_context.streams()[video_stream_index]; 31 | let decoder = AVCodec::find_decoder(video_stream.codecpar().codec_id) 32 | .context("Cannot find the decoder for video stream")?; 33 | 34 | let mut decode_context = AVCodecContext::new(&decoder); 35 | decode_context.apply_codecpar(&video_stream.codecpar())?; 36 | decode_context.open(None)?; 37 | decode_context 38 | }; 39 | 40 | let image_buffer = AVImage::new( 41 | ffi::AV_PIX_FMT_RGB32, 42 | decode_context.width, 43 | decode_context.height, 44 | 1, 45 | ) 46 | .context("Failed to create image buffer.")?; 47 | 48 | let mut frame_rgb = AVFrameWithImage::new(image_buffer); 49 | 50 | let mut sws_context = SwsContext::get_context( 51 | decode_context.width, 52 | decode_context.height, 53 | decode_context.pix_fmt, 54 | decode_context.width, 55 | decode_context.height, 56 | ffi::AV_PIX_FMT_RGB32, 57 | ffi::SWS_BILINEAR, 58 | None, 59 | None, 60 | None, 61 | ) 62 | .context("Failed to create a swscale context.")?; 63 | 64 | let mut i = 0; 65 | while let Some(packet) = input_format_context.read_packet().unwrap() { 66 | if packet.stream_index != video_stream_index as i32 { 67 | continue; 68 | } 69 | decode_context.send_packet(Some(&packet))?; 70 | loop { 71 | let frame = match decode_context.receive_frame() { 72 | Ok(frame) => frame, 73 | Err(RsmpegError::DecoderDrainError) | Err(RsmpegError::DecoderFlushedError) => { 74 | break; 75 | } 76 | Err(e) => return Err(e.into()), 77 | }; 78 | 79 | sws_context.scale_frame(&frame, 0, decode_context.height, &mut frame_rgb)?; 80 | if i >= 5 { 81 | break; 82 | } 83 | 84 | i += 1; 85 | // Save the frame as a ppm file 86 | avio::pgm_save(&frame_rgb, &format!("{}/frame{}.ppm", out_dir, i))?; 87 | } 88 | } 89 | Ok(()) 90 | } 91 | 92 | #[cfg(test)] 93 | mod tests { 94 | use super::*; 95 | 96 | #[test] 97 | #[ignore = "tutorial01_test0 测试运行依赖测试文件,暂时忽略"] 98 | fn tutorial01_test0() { 99 | dump_frame( 100 | c"tests/assets/vids/centaur.mpg", 101 | "tests/output/tutorial01/centaur", 102 | ) 103 | .unwrap(); 104 | } 105 | 106 | #[test] 107 | #[ignore = "tutorial01_test1 测试运行依赖测试文件,暂时忽略"] 108 | fn tutorial01_test1() { 109 | dump_frame( 110 | c"tests/assets/vids/bear.mp4", 111 | "tests/output/tutorial01/bear", 112 | ) 113 | .unwrap(); 114 | } 115 | 116 | #[test] 117 | #[ignore = "tutorial01_test2 测试运行依赖测试文件,暂时忽略"] 118 | fn tutorial01_test2() { 119 | dump_frame( 120 | c"tests/assets/vids/mov_sample.mov", 121 | "tests/output/tutorial01/mov_sample", 122 | ) 123 | .unwrap(); 124 | } 125 | 126 | #[test] 127 | #[ignore = "tutorial01_test3 测试运行依赖测试文件,暂时忽略"] 128 | fn tutorial01_test3() { 129 | dump_frame(c"tests/assets/vids/vp8.mp4", "tests/output/tutorial01/vp8").unwrap(); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /examples/mod.rs: -------------------------------------------------------------------------------- 1 | //! misc examples 2 | //! 3 | //! see 4 | //! 5 | #![allow(dead_code)] 6 | #![allow(unused_variables)] 7 | 8 | pub mod misc; 9 | 10 | fn main() { 11 | println!("misc") 12 | } 13 | -------------------------------------------------------------------------------- /examples/muxing.rs: -------------------------------------------------------------------------------- 1 | use rsmedia::{ 2 | hwaccel::HWDeviceType, 3 | mux::{Demuxer, Muxer}, 4 | EncoderBuilder, MediaType, Options, PixelFormat, SampleFormat, StreamWriterBuilder, 5 | }; 6 | 7 | use std::path::Path; 8 | 9 | fn main() { 10 | tracing_subscriber::fmt() 11 | .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) 12 | .with_timer(tracing_subscriber::fmt::time::ChronoLocal::rfc_3339()) 13 | .with_target(true) 14 | .with_file(true) 15 | .with_line_number(true) 16 | .with_thread_ids(true) 17 | .init(); 18 | 19 | rsmedia::init().unwrap(); 20 | 21 | let input_path = Path::new("/tmp/bear.mp4"); 22 | let mut demuxer = Demuxer::new(input_path).unwrap(); 23 | 24 | let output_path = Path::new("/tmp/output.mov"); 25 | let stream_writer = StreamWriterBuilder::new(output_path) 26 | .with_format("mov") 27 | .with_options(Some(Options::preset_avformat_fragmented_mov())) 28 | .build() 29 | .unwrap(); 30 | let mut muxer = Muxer::new_from_writer(stream_writer); 31 | 32 | // add all streams from input to output muxer 33 | for in_stream in demuxer.streams() { 34 | let stream_info = &in_stream.stream_info; 35 | 36 | let encoder = { 37 | if stream_info.media_type == MediaType::VIDEO { 38 | // build video encoder 39 | EncoderBuilder::new_video(stream_info.width as usize, stream_info.height as usize) 40 | // cuda acceleration 41 | .with_hardware_device(Some(HWDeviceType::CUDA.auto_best_config().unwrap())) 42 | .with_codec_name(Some("h264_nvenc".to_string())) 43 | // notes: options must be match with input video encoder codec, 44 | .with_options(Some(Options::preset_h264_nvenc())) 45 | .with_bit_rate(stream_info.bit_rate) 46 | // video 47 | .with_time_base_ra(stream_info.time_base) 48 | .with_frame_rate_ra(stream_info.frame_rate) 49 | .with_pixel_format(PixelFormat::from(stream_info.format)) 50 | .build() 51 | .unwrap() 52 | } else if stream_info.media_type == MediaType::AUDIO { 53 | // build audio encoder 54 | EncoderBuilder::new_audio( 55 | stream_info.bit_rate, 56 | stream_info.channel_layout.nb_channels, 57 | stream_info.sample_rate, 58 | SampleFormat::from(stream_info.format), 59 | ) 60 | .build() 61 | .unwrap() 62 | } else { 63 | panic!("Unsupported media type: {:?}", stream_info.media_type); 64 | } 65 | }; 66 | 67 | let stream_index = muxer.add_stream(encoder).unwrap(); 68 | muxer.dump(stream_index).unwrap() 69 | } 70 | 71 | // demux and mux all frames from input to output muxer 72 | loop { 73 | match demuxer.demux() { 74 | Ok(Some((stream_index, frame))) => { 75 | println!("stream index:{}, {:?}", stream_index, frame); 76 | let _ = muxer.mux(frame, stream_index).unwrap(); 77 | } 78 | Ok(None) => { 79 | log::info!("End of input file"); 80 | break; 81 | } 82 | Err(e) => { 83 | eprintln!("Demuxing error: {}", e); 84 | break; 85 | } 86 | } 87 | } 88 | 89 | // finish muxing 90 | muxer.finish().unwrap(); 91 | } 92 | -------------------------------------------------------------------------------- /fonts/Arial.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dromara/rsmedia/184f21cccb4a3041a9e38a19a17de7275ae87d0b/fonts/Arial.ttf -------------------------------------------------------------------------------- /fonts/DejaVuSansMono.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dromara/rsmedia/184f21cccb4a3041a9e38a19a17de7275ae87d0b/fonts/DejaVuSansMono.ttf -------------------------------------------------------------------------------- /src/flags.rs: -------------------------------------------------------------------------------- 1 | use crate::utils; 2 | 3 | use rsmpeg::avutil; 4 | use rsmpeg::ffi; 5 | 6 | #[repr(u32)] 7 | #[allow(non_camel_case_types)] 8 | #[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] 9 | pub enum AvPacketFlags { 10 | KEY = ffi::AV_PKT_FLAG_KEY, 11 | CORRUPT = ffi::AV_PKT_FLAG_CORRUPT, 12 | DISCARD = ffi::AV_PKT_FLAG_DISCARD, 13 | TRUSTED = ffi::AV_PKT_FLAG_TRUSTED, 14 | DISPOSABLE = ffi::AV_PKT_FLAG_DISPOSABLE, 15 | } 16 | 17 | #[repr(u32)] 18 | #[allow(non_camel_case_types)] 19 | #[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] 20 | pub enum AvCodecFlags { 21 | UNALIGNED = ffi::AV_CODEC_FLAG_UNALIGNED, 22 | QSCALE = ffi::AV_CODEC_FLAG_QSCALE, 23 | _4MV = ffi::AV_CODEC_FLAG_4MV, 24 | OUTPUT_CORRUPT = ffi::AV_CODEC_FLAG_OUTPUT_CORRUPT, 25 | QPEL = ffi::AV_CODEC_FLAG_QPEL, 26 | PASS1 = ffi::AV_CODEC_FLAG_PASS1, 27 | PASS2 = ffi::AV_CODEC_FLAG_PASS2, 28 | GRAY = ffi::AV_CODEC_FLAG_GRAY, 29 | PSNR = ffi::AV_CODEC_FLAG_PSNR, 30 | // #[cfg(not(feature = "ffmpeg_6_0"))] 31 | // TRUNCATED = ffi::AV_CODEC_FLAG_TRUNCATED, 32 | INTERLACED_DCT = ffi::AV_CODEC_FLAG_INTERLACED_DCT, 33 | LOW_DELAY = ffi::AV_CODEC_FLAG_LOW_DELAY, 34 | GLOBAL_HEADER = ffi::AV_CODEC_FLAG_GLOBAL_HEADER, 35 | BITEXACT = ffi::AV_CODEC_FLAG_BITEXACT, 36 | AC_PRED = ffi::AV_CODEC_FLAG_AC_PRED, 37 | LOOP_FILTER = ffi::AV_CODEC_FLAG_LOOP_FILTER, 38 | INTERLACED_ME = ffi::AV_CODEC_FLAG_INTERLACED_ME, 39 | CLOSED_GOP = ffi::AV_CODEC_FLAG_CLOSED_GOP, 40 | } 41 | 42 | #[repr(u32)] 43 | #[allow(non_camel_case_types)] 44 | #[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] 45 | pub enum AvFormatFlags { 46 | NO_FILE = ffi::AVFMT_NOFILE, 47 | NEED_NUMBER = ffi::AVFMT_NEEDNUMBER, 48 | SHOW_IDS = ffi::AVFMT_SHOW_IDS, 49 | // #[cfg(not(feature = "ffmpeg_4_0"))] 50 | // RAW_PICTURE = ffi::AVFMT_RAWPICTURE, 51 | GLOBAL_HEADER = ffi::AVFMT_GLOBALHEADER, 52 | NO_TIMESTAMPS = ffi::AVFMT_NOTIMESTAMPS, 53 | GENERIC_INDEX = ffi::AVFMT_GENERIC_INDEX, 54 | TS_DISCONT = ffi::AVFMT_TS_DISCONT, 55 | VARIABLE_FPS = ffi::AVFMT_VARIABLE_FPS, 56 | NO_DIMENSIONS = ffi::AVFMT_NODIMENSIONS, 57 | NO_STREAMS = ffi::AVFMT_NOSTREAMS, 58 | NO_BINSEARCH = ffi::AVFMT_NOBINSEARCH, 59 | NO_GENSEARCH = ffi::AVFMT_NOGENSEARCH, 60 | NO_BYTE_SEEK = ffi::AVFMT_NO_BYTE_SEEK, 61 | ALLOW_FLUSH = ffi::AVFMT_ALLOW_FLUSH, 62 | TS_NONSTRICT = ffi::AVFMT_TS_NONSTRICT, 63 | TS_NEGATIVE = ffi::AVFMT_TS_NEGATIVE, 64 | SEEK_TO_PTS = ffi::AVFMT_SEEK_TO_PTS, 65 | } 66 | 67 | #[repr(u32)] 68 | #[allow(non_camel_case_types)] 69 | #[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] 70 | pub enum AvFormatContextFlags { 71 | GENPTS = ffi::AVFMT_FLAG_GENPTS, 72 | IGNIDX = ffi::AVFMT_FLAG_IGNIDX, 73 | NONBLOCK = ffi::AVFMT_FLAG_NONBLOCK, 74 | IGNDTS = ffi::AVFMT_FLAG_IGNDTS, 75 | NOFILLIN = ffi::AVFMT_FLAG_NOFILLIN, 76 | NOPARSE = ffi::AVFMT_FLAG_NOPARSE, 77 | NOBUFFER = ffi::AVFMT_FLAG_NOBUFFER, 78 | CUSTOM_IO = ffi::AVFMT_FLAG_CUSTOM_IO, 79 | DISCARD_CORRUPT = ffi::AVFMT_FLAG_DISCARD_CORRUPT, 80 | FLUSH_PACKETS = ffi::AVFMT_FLAG_FLUSH_PACKETS, 81 | BITEXACT = ffi::AVFMT_FLAG_BITEXACT, 82 | SORT_DTS = ffi::AVFMT_FLAG_SORT_DTS, 83 | FAST_SEEK = ffi::AVFMT_FLAG_FAST_SEEK, 84 | SHORTEST = ffi::AVFMT_FLAG_SHORTEST, 85 | AUTO_BSF = ffi::AVFMT_FLAG_AUTO_BSF, 86 | } 87 | 88 | #[repr(u32)] 89 | #[allow(non_camel_case_types)] 90 | #[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] 91 | pub enum AvScalerFlags { 92 | FAST_BILINEAR = ffi::SWS_FAST_BILINEAR, 93 | BILINEAR = ffi::SWS_BILINEAR, 94 | BICUBIC = ffi::SWS_BICUBIC, 95 | X = ffi::SWS_X, 96 | POINT = ffi::SWS_POINT, 97 | AREA = ffi::SWS_AREA, 98 | BICUBLIN = ffi::SWS_BICUBLIN, 99 | GAUSS = ffi::SWS_GAUSS, 100 | SINC = ffi::SWS_SINC, 101 | LANCZOS = ffi::SWS_LANCZOS, 102 | SPLINE = ffi::SWS_SPLINE, 103 | SRC_V_CHR_DROP_MASK = ffi::SWS_SRC_V_CHR_DROP_MASK, 104 | // alias POINT=16 105 | // SRC_V_CHR_DROP_SHIFT = ffi::SWS_SRC_V_CHR_DROP_SHIFT, 106 | PARAM_DEFAULT = ffi::SWS_PARAM_DEFAULT, 107 | PRINT_INFO = ffi::SWS_PRINT_INFO, 108 | FULL_CHR_H_INT = ffi::SWS_FULL_CHR_H_INT, 109 | FULL_CHR_H_INP = ffi::SWS_FULL_CHR_H_INP, 110 | DIRECT_BGR = ffi::SWS_DIRECT_BGR, 111 | ACCURATE_RND = ffi::SWS_ACCURATE_RND, 112 | BITEXACT = ffi::SWS_BITEXACT, 113 | ERROR_DIFFUSION = ffi::SWS_ERROR_DIFFUSION, 114 | } 115 | 116 | #[repr(u32)] 117 | #[allow(non_camel_case_types)] 118 | #[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] 119 | pub enum AvDispositionFlags { 120 | DEFAULT = ffi::AV_DISPOSITION_DEFAULT, 121 | DUB = ffi::AV_DISPOSITION_DUB, 122 | ORIGINAL = ffi::AV_DISPOSITION_ORIGINAL, 123 | COMMENT = ffi::AV_DISPOSITION_COMMENT, 124 | LYRICS = ffi::AV_DISPOSITION_LYRICS, 125 | KARAOKE = ffi::AV_DISPOSITION_KARAOKE, 126 | FORCED = ffi::AV_DISPOSITION_FORCED, 127 | HEARING_IMPAIRED = ffi::AV_DISPOSITION_HEARING_IMPAIRED, 128 | VISUAL_IMPAIRED = ffi::AV_DISPOSITION_VISUAL_IMPAIRED, 129 | CLEAN_EFFECTS = ffi::AV_DISPOSITION_CLEAN_EFFECTS, 130 | ATTACHED_PIC = ffi::AV_DISPOSITION_ATTACHED_PIC, 131 | CAPTIONS = ffi::AV_DISPOSITION_CAPTIONS, 132 | DESCRIPTIONS = ffi::AV_DISPOSITION_DESCRIPTIONS, 133 | METADATA = ffi::AV_DISPOSITION_METADATA, 134 | #[cfg(feature = "ffmpeg7")] 135 | MULTILAYER = ffi::AV_DISPOSITION_MULTILAYER, 136 | } 137 | 138 | #[repr(i32)] 139 | #[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] 140 | pub enum MediaType { 141 | UNKNOWN = ffi::AVMEDIA_TYPE_UNKNOWN, 142 | VIDEO = ffi::AVMEDIA_TYPE_VIDEO, 143 | AUDIO = ffi::AVMEDIA_TYPE_AUDIO, 144 | DATA = ffi::AVMEDIA_TYPE_DATA, 145 | SUBTITLE = ffi::AVMEDIA_TYPE_SUBTITLE, 146 | ATTACHMENT = ffi::AVMEDIA_TYPE_ATTACHMENT, 147 | } 148 | 149 | impl MediaType { 150 | pub fn get_media_type_string(&self) -> String { 151 | avutil::get_media_type_string(*self as _) 152 | .map_or("Unknown".to_string(), |s| utils::to_string(s).unwrap()) 153 | } 154 | } 155 | 156 | impl From for MediaType { 157 | fn from(item: ffi::AVMediaType) -> Self { 158 | match item { 159 | ffi::AVMEDIA_TYPE_UNKNOWN => MediaType::UNKNOWN, 160 | ffi::AVMEDIA_TYPE_VIDEO => MediaType::VIDEO, 161 | ffi::AVMEDIA_TYPE_AUDIO => MediaType::AUDIO, 162 | ffi::AVMEDIA_TYPE_DATA => MediaType::DATA, 163 | ffi::AVMEDIA_TYPE_SUBTITLE => MediaType::SUBTITLE, 164 | ffi::AVMEDIA_TYPE_ATTACHMENT => MediaType::ATTACHMENT, 165 | _ => panic!("Invalid media type"), 166 | } 167 | } 168 | } 169 | 170 | #[repr(i32)] 171 | #[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)] 172 | pub enum SampleFormat { 173 | /// < none 174 | NONE = ffi::AV_SAMPLE_FMT_NONE, 175 | /// < unsigned 8 bits 176 | U8 = ffi::AV_SAMPLE_FMT_U8, 177 | /// < signed 16 bits 178 | S16 = ffi::AV_SAMPLE_FMT_S16, 179 | /// < signed 32 bits 180 | S32 = ffi::AV_SAMPLE_FMT_S32, 181 | /// < float 182 | FLT = ffi::AV_SAMPLE_FMT_FLT, 183 | /// < double 184 | DBL = ffi::AV_SAMPLE_FMT_DBL, 185 | /// < unsigned 8 bits, planar 186 | U8P = ffi::AV_SAMPLE_FMT_U8P, 187 | /// < signed 16 bits, planar 188 | S16P = ffi::AV_SAMPLE_FMT_S16P, 189 | /// < signed 32 bits, planar 190 | S32P = ffi::AV_SAMPLE_FMT_S32P, 191 | /// < float, planar 192 | FLTP = ffi::AV_SAMPLE_FMT_FLTP, 193 | /// < double, planar 194 | DBLP = ffi::AV_SAMPLE_FMT_DBLP, 195 | /// < signed 64 bits 196 | S64 = ffi::AV_SAMPLE_FMT_S64, 197 | /// < signed 64 bits, planar 198 | S64P = ffi::AV_SAMPLE_FMT_S64P, 199 | } 200 | 201 | impl SampleFormat { 202 | pub fn is_planar(&self) -> bool { 203 | avutil::sample_fmt_is_planar(*self as _) 204 | } 205 | 206 | pub fn get_bytes_per_sample(&self) -> Option { 207 | avutil::get_bytes_per_sample(*self as _) 208 | } 209 | 210 | pub fn get_sample_fmt_name(&self) -> String { 211 | avutil::get_sample_fmt_name(*self as _) 212 | .map_or("Unknown".to_string(), |s| utils::to_string(s).unwrap()) 213 | } 214 | 215 | pub fn get_packed_sample_fmt(&self) -> Option { 216 | avutil::get_packed_sample_fmt(*self as _).map(SampleFormat::from) 217 | } 218 | 219 | pub fn get_planar_sample_fmt(&self) -> Option { 220 | avutil::get_planar_sample_fmt(*self as _).map(SampleFormat::from) 221 | } 222 | } 223 | 224 | impl From for SampleFormat { 225 | fn from(item: ffi::AVSampleFormat) -> Self { 226 | match item { 227 | ffi::AV_SAMPLE_FMT_NONE => SampleFormat::NONE, 228 | ffi::AV_SAMPLE_FMT_U8 => SampleFormat::U8, 229 | ffi::AV_SAMPLE_FMT_S16 => SampleFormat::S16, 230 | ffi::AV_SAMPLE_FMT_S32 => SampleFormat::S32, 231 | ffi::AV_SAMPLE_FMT_FLT => SampleFormat::FLT, 232 | ffi::AV_SAMPLE_FMT_DBL => SampleFormat::DBL, 233 | ffi::AV_SAMPLE_FMT_U8P => SampleFormat::U8P, 234 | ffi::AV_SAMPLE_FMT_S16P => SampleFormat::S16P, 235 | ffi::AV_SAMPLE_FMT_S32P => SampleFormat::S32P, 236 | ffi::AV_SAMPLE_FMT_FLTP => SampleFormat::FLTP, 237 | ffi::AV_SAMPLE_FMT_DBLP => SampleFormat::DBLP, 238 | ffi::AV_SAMPLE_FMT_S64 => SampleFormat::S64, 239 | ffi::AV_SAMPLE_FMT_S64P => SampleFormat::S64P, 240 | _ => panic!("Invalid sample format"), 241 | } 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /src/init.rs: -------------------------------------------------------------------------------- 1 | use crate::io::init_logging; 2 | use anyhow::{Error, Result}; 3 | use once_cell::sync::OnceCell; 4 | 5 | static INIT: OnceCell<()> = OnceCell::new(); 6 | 7 | /// Initialize global ffmpeg settings. This also intializes the 8 | /// logging capability and redirect it to `tracing`. 9 | pub fn init() -> Result<()> { 10 | INIT.get_or_try_init(|| { 11 | // ffmpeg::init()?; 12 | 13 | // Redirect logging to the Rust `tracing` crate. 14 | init_logging(); 15 | 16 | Ok::<(), Error>(()) 17 | })?; 18 | 19 | Ok(()) 20 | } 21 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod decode; 2 | pub mod encode; 3 | #[cfg(feature = "ndarray")] 4 | pub mod frame; 5 | #[cfg(feature = "ndarray")] 6 | pub use frame::MediaFrame; 7 | pub mod codec; 8 | pub mod colors; 9 | pub mod filter; 10 | pub mod flags; 11 | pub mod hwaccel; 12 | pub mod imgutils; 13 | pub mod init; 14 | pub mod io; 15 | pub mod location; 16 | pub mod mux; 17 | pub mod options; 18 | pub mod pixel; 19 | pub mod resize; 20 | pub mod stream; 21 | pub mod swctx; 22 | pub mod time; 23 | pub mod utils; 24 | 25 | pub use decode::{Decoder, DecoderBuilder}; 26 | pub use encode::{Encoder, EncoderBuilder}; 27 | pub use flags::{MediaType, SampleFormat}; 28 | pub use init::init; 29 | pub use io::{Reader, Writer}; 30 | pub use io::{StreamReader, StreamReaderBuilder, StreamWriter, StreamWriterBuilder}; 31 | pub use location::{Location, Url}; 32 | pub use options::Options; 33 | pub use pixel::PixelFormat; 34 | pub use resize::Resize; 35 | pub use time::Time; 36 | 37 | /// Re-export internal definition for caller to use. 38 | pub use rsmpeg::avutil; 39 | -------------------------------------------------------------------------------- /src/location.rs: -------------------------------------------------------------------------------- 1 | /// Re-export [`url::Url`] since it is an input type for callers of the API. 2 | pub use url::Url; 3 | 4 | /// Represents a video file or stream location. Can be either a file resource (a path) or a network 5 | /// resource (a URL). 6 | #[derive(Debug, Clone, Hash, PartialEq, Eq)] 7 | pub enum Location { 8 | /// File source. 9 | File(std::path::PathBuf), 10 | /// Network source. 11 | Network(Url), 12 | } 13 | 14 | impl Location { 15 | /// Coerce underlying location to a path. 16 | /// 17 | /// This will create a path with a URL in it (which is kind of weird but we use it to pass on 18 | /// URLs to ffmpeg). 19 | pub fn as_path(&self) -> &std::path::Path { 20 | match self { 21 | Location::File(path) => path.as_path(), 22 | Location::Network(url) => std::path::Path::new(url.as_str()), 23 | } 24 | } 25 | } 26 | 27 | impl From<&Location> for Location { 28 | fn from(value: &Location) -> Location { 29 | value.clone() 30 | } 31 | } 32 | 33 | impl From for Location { 34 | fn from(value: std::path::PathBuf) -> Location { 35 | Location::File(value) 36 | } 37 | } 38 | 39 | impl From<&std::path::Path> for Location { 40 | fn from(value: &std::path::Path) -> Location { 41 | Location::File(value.to_path_buf()) 42 | } 43 | } 44 | 45 | impl From for Location { 46 | fn from(value: Url) -> Location { 47 | Location::Network(value) 48 | } 49 | } 50 | 51 | impl From<&Url> for Location { 52 | fn from(value: &Url) -> Location { 53 | Location::Network(value.clone()) 54 | } 55 | } 56 | 57 | impl std::fmt::Display for Location { 58 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 59 | match self { 60 | Location::File(path) => write!(f, "{}", path.display()), 61 | Location::Network(url) => write!(f, "{url}"), 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/options.rs: -------------------------------------------------------------------------------- 1 | use crate::utils; 2 | 3 | use rsmpeg::avutil::AVDictionary; 4 | 5 | use std::collections::HashMap; 6 | 7 | /// A macro to create a HashMap with key-value pairs. 8 | macro_rules! map { 9 | // empty 10 | () => { 11 | ::std::collections::HashMap::new() 12 | }; 13 | // key-value pairs 14 | ($($key:expr => $value:expr),+ $(,)?) => { 15 | { 16 | let mut map = ::std::collections::HashMap::new(); 17 | $( 18 | map.insert($key.into(), $value.into()); 19 | )+ 20 | map 21 | } 22 | }; 23 | } 24 | 25 | /// A wrapper type for ffmpeg options. 26 | /// 27 | /// FFmpeg Documentation: 28 | /// 29 | /// `libavformat/options_table.h`: 30 | /// `libavcodec/options_table.h`: 31 | #[derive(Clone)] 32 | pub struct Options(AVDictionary); 33 | 34 | impl Options { 35 | pub fn new(dict: AVDictionary) -> Self { 36 | Self(dict) 37 | } 38 | 39 | /// Creates options such that ffmpeg will prefer TCP transport when reading RTSP stream (over 40 | /// the default UDP format). It also adds some options to reduce the socket and I/O timeouts to 41 | /// 4 seconds. 42 | /// 43 | /// This sets the `rtsp_transport` to `tcp` in ffmpeg options, 44 | /// it also sets `rw_timeout` and `stimeout` to lower (more sane) values. 45 | pub fn preset_avformat_rtsp_transport_tcp() -> Self { 46 | let opts = map! { 47 | "rtsp_transport" => "tcp", 48 | // These can't be too low because ffmpeg takes its sweet time 49 | "rw_timeout" => "16000000", 50 | "stimeout" => "16000000", 51 | }; 52 | 53 | // HashMap -> Options 54 | opts.into() 55 | } 56 | 57 | /// Creates options such that ffmpeg is instructed to fragment output and mux to fragmented mp4 58 | /// container format. 59 | /// 60 | /// This modifies the `movflags` key to supported fragmented output. The muxer output will not 61 | /// have a header and each packet contains enough metadata to be streamed without the header. 62 | /// Muxer output should be compatiable with MSE. 63 | pub fn preset_avformat_fragmented_mov() -> Self { 64 | let opts = map! { 65 | "movflags" => "faststart+frag_keyframe+frag_custom+empty_moov+omit_tfhd_offset", 66 | }; 67 | 68 | // HashMap -> Options 69 | opts.into() 70 | } 71 | 72 | /// Creates options for a FLV muxer. 73 | pub fn preset_avformat_flv() -> Self { 74 | let opts = map! { 75 | "flvflags" => "no_duration_filesize", 76 | "fflags" => "nobuffer+flush_packets", 77 | // 添加实时流标志 78 | "live" => "1", 79 | // 完全禁用元数据更新 80 | "write_metaf" => "0", 81 | // 设置较小的chunk大小以减少延迟 82 | "chunk_size" => "4096", 83 | }; 84 | 85 | // HashMap -> Options 86 | opts.into() 87 | } 88 | 89 | /// Default avcodec options for a libx264 encoder. 90 | pub fn preset_h264() -> Self { 91 | let opts = map! { 92 | // ultrafast,superfast,veryfast,faster,fast,medium,slow,slower,veryslow,placebo 93 | "preset" => "medium", 94 | // baseline,main,high 95 | "profile:v" => "high", 96 | // 场景切换敏感度 97 | "scenecut" => "0", 98 | }; 99 | 100 | // HashMap -> Options 101 | opts.into() 102 | } 103 | 104 | /// Options for a libx264 encoder that are tuned for low-latency encoding such as for real-time streaming. 105 | pub fn preset_h264_realtime() -> Self { 106 | let opts = map! { 107 | // ultrafast,superfast,veryfast,faster,fast,medium,slow,slower,veryslow,placebo 108 | "preset" => "medium", 109 | // baseline,main,high 110 | "profile:v" => "main", 111 | // film,animation,grain,stillimage,psnr,ssim,fastdecode,zerolatency 112 | "tune" => "zerolatency", 113 | // rc 参数在 libx264 中不是直接支持的,应该使用 rate_control 或相关参数如 crf, qp 或 bitrate 114 | // crf, vbr, cbr, abr 115 | // "rc" => "cbr", 116 | // 设置比特率控制,视频比特率 117 | "b:v" => "3000k", 118 | // 最大比特率 119 | "maxrate" => "3500k", 120 | // 缓冲区大小 121 | "bufsize" => "3000k", 122 | // 恒定质量因子 123 | "crf" => "23", 124 | // 场景切换敏感度 125 | "scenecut" => "0", 126 | // 周期内部刷新替代关键帧 127 | "intra-refresh" => "1", 128 | // 参考帧数量 129 | "refs" => "3", 130 | // GOP=60(2秒@30fps) 131 | "g" => "60", 132 | // 禁用 B 帧 133 | "bf" => "0", 134 | // 最小量化参数 135 | "qmin" => "4", 136 | // 最大量化参数 137 | "qmax" => "51", 138 | // 启用中等强度去块滤波 139 | "deblock" => "1:1", 140 | // 自适应量化模式 141 | "aq-mode" => "2", 142 | // 量化优化, 0: 禁用, 1: 仅用于最终编码, 2: 用于所有模式决策 143 | "trellis" => "1", 144 | "threads" => "auto", 145 | // 使用所有可用的分区模式 146 | "partitions" => "all", 147 | // 最小关键帧间隔 148 | "keyint_min" => "30", 149 | // 强制恒定帧率 150 | "force-cfr" => "1", 151 | // 启用切片线程 152 | "sliced_threads" => "1", 153 | // 禁用前瞻同步 154 | "sync-lookahead" => "0", 155 | // 减少前瞻帧数 156 | "rc-lookahead" => "10", 157 | // 使用x264opts组合参数进行更精细的控制 158 | "x264opts" => "no-mbtree:no-weightb:nal-hrd=cbr", 159 | }; 160 | 161 | // HashMap -> Options 162 | opts.into() 163 | } 164 | 165 | /// h264_nvenc options only 166 | /// 167 | /// FFMpeg with NVENC: 168 | /// 169 | /// 170 | /// NVENC Preset Migration Guide: 171 | /// 172 | /// 173 | pub fn preset_h264_nvenc() -> Self { 174 | let opts = map! { 175 | // p1-p7, default(p4), slow, medium, fast, hp, hq, bd, ll, llhq, llhp, lossless 176 | "preset" => "p5", 177 | // baseline, main, high, high444p, high10, high422 178 | "profile" => "high", 179 | // ll, ull, lossless, film, animation, grain, fastdecode, zerolatency, hq 180 | "tune" => "ll", 181 | // 设置比特率 4Mbps 182 | "b:v" => "4000k", 183 | "maxrate" => "5000k", 184 | "bufsize" => "8000k", 185 | // constqp, ll_2pass_size, ll_2pass_quality 186 | // vbr, vbr_hq, vbr_minqp, vbr_2pass 187 | // cbr, cbr_hq, cbr_ld_hq 188 | "rc" => "cbr", 189 | // 添加锐度增强,低延迟不需要,增强画质可以启用 190 | // "rc-lookahead" => "20", 191 | // 量化参数 192 | "qmin" => "10", 193 | "qmax" => "18", 194 | // 启用自适应量化 195 | "spatial-aq" => "1", 196 | "temporal-aq" => "1", 197 | "aq-strength" => "8", 198 | // 启用2pass编码,注意与 zerolatency 可能冲突 199 | // "2pass" => "1", 200 | // GOP设置,较小的GOP有利于快速恢复和低延迟 201 | "g" => "30", 202 | // 禁用B帧以,避免出现画面闪烁 203 | "bf" => "0", 204 | "b_ref_mode" => "middle", 205 | // 启用场景切换检测,允许在场景变化时插入I帧 206 | "no-scenecut" => "0", 207 | // 低延时 208 | "delay" => "0", 209 | "zerolatency" => "1", 210 | // NVENC特有的参数 211 | // 增加表面缓冲区数量 212 | "surfaces" => "32", 213 | // 加权预测,改善低光照 214 | "weighted_pred" => "1", 215 | }; 216 | 217 | // HashMap -> Options 218 | opts.into() 219 | } 220 | 221 | /// 转换为 AVDictionary 但不转移所有权 222 | pub fn as_dict(&self) -> &AVDictionary { 223 | &self.0 224 | } 225 | 226 | /// 转换为 AVDictionary 并转移所有权 227 | pub fn into_dict(self) -> AVDictionary { 228 | self.0 229 | } 230 | 231 | /// 创建一个 AVDictionary 的副本 232 | pub fn to_dict(&self) -> AVDictionary { 233 | self.0.clone() 234 | } 235 | } 236 | 237 | /// HashMap -> Options 238 | impl From> for Options { 239 | /// Converts from `HashMap` to `Options`. 240 | /// 241 | /// # Arguments 242 | /// 243 | /// * `item` - Item to convert from. 244 | /// 245 | /// # Example 246 | /// 247 | /// ```ignore 248 | /// let my_opts = HashMap::new(); 249 | /// options.insert( 250 | /// "my_option".to_string(), 251 | /// "my_value".to_string(), 252 | /// ); 253 | /// 254 | /// let opts: Options = my_opts.into(); 255 | /// ``` 256 | fn from(item: HashMap) -> Self { 257 | let mut dict = AVDictionary::new(c"", c"", 0); 258 | for (k, v) in item { 259 | dict = dict.set(&utils::from_str(&k), &utils::from_str(&v), 0); 260 | } 261 | Self(dict) 262 | } 263 | } 264 | 265 | /// Converts from `&Options` to `HashMap`. 266 | impl From<&Options> for HashMap { 267 | fn from(item: &Options) -> Self { 268 | item.0 269 | .into_iter() 270 | .map(|entry| { 271 | ( 272 | utils::to_string(entry.key()).unwrap(), 273 | utils::to_string(entry.value()).unwrap(), 274 | ) 275 | }) 276 | .collect() 277 | } 278 | } 279 | 280 | /// `Options` -> `HashMap` 281 | impl From for HashMap { 282 | /// Converts from `Options` to `HashMap`. 283 | /// 284 | /// # Arguments 285 | /// 286 | /// * `item` - Item to convert from. 287 | fn from(item: Options) -> Self { 288 | (&item).into() 289 | } 290 | } 291 | 292 | impl std::fmt::Debug for Options { 293 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 294 | let dict: HashMap = self.into(); 295 | write!(f, "{:?}", dict) 296 | } 297 | } 298 | 299 | impl std::fmt::Display for Options { 300 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 301 | std::fmt::Debug::fmt(self, f) 302 | } 303 | } 304 | 305 | unsafe impl Send for Options {} 306 | unsafe impl Sync for Options {} 307 | 308 | #[cfg(test)] 309 | mod tests { 310 | use super::*; 311 | 312 | #[test] 313 | fn test_options_debug() { 314 | let opts = Options::preset_h264_realtime(); 315 | println!("{:?}", opts); 316 | } 317 | } 318 | -------------------------------------------------------------------------------- /src/resize.rs: -------------------------------------------------------------------------------- 1 | /// Represents width and height in a tuple. 2 | type Dims = (u32, u32); 3 | 4 | /// Represents the possible resize strategies. 5 | #[derive(Debug, Copy, Clone, PartialEq, Eq)] 6 | pub enum Resize { 7 | /// When resizing with `Resize::Exact`, each frame will be resized to the exact width and height 8 | /// given, without taking into account aspect ratio. 9 | Exact(u32, u32), 10 | /// When resizing with `Resize::Fit`, each frame will be resized to the biggest width and height 11 | /// possible within the given dimensions, without changing the aspect ratio. 12 | Fit(u32, u32), 13 | /// When resizing with `Resize::FitEven`, each frame will be resized to the biggest even width 14 | /// and height possible within the given dimensions, maintaining aspect ratio. Resizing using 15 | /// this method can fail if there exist no dimensions that fit these constraints. 16 | /// 17 | /// Note that this resizing method is especially useful since some encoders only accept frames 18 | /// with dimensions that are divisible by 2. 19 | FitEven(u32, u32), 20 | } 21 | 22 | impl Resize { 23 | /// Compute the dimensions after resizing depending on the resize strategy. 24 | /// 25 | /// # Arguments 26 | /// 27 | /// * `dims` - Input dimensions (width and height). 28 | /// 29 | /// # Return value 30 | /// 31 | /// Tuple of width and height with dimensions after resizing. 32 | pub fn compute_for(self, dims: Dims) -> Option { 33 | match self { 34 | Resize::Exact(w, h) => Some((w, h)), 35 | Resize::Fit(w, h) => calculate_fit_dims(dims, (w, h)), 36 | Resize::FitEven(w, h) => calculate_fit_dims_even(dims, (w, h)), 37 | } 38 | } 39 | } 40 | 41 | /// Calculates the maximum image dimensions `w` and `h` that fit inside `w_max` and `h_max` 42 | /// retaining the original aspect ratio. 43 | /// 44 | /// # Arguments 45 | /// 46 | /// * `dims` - Original dimensions: width and height. 47 | /// * `fit_dims` - Dimensions to fit in: width and height. 48 | /// 49 | /// # Return value 50 | /// 51 | /// The fitted dimensions if they exist and are positive and more than zero. 52 | fn calculate_fit_dims(dims: (u32, u32), fit_dims: (u32, u32)) -> Option<(u32, u32)> { 53 | let (w, h) = dims; 54 | let (w_max, h_max) = fit_dims; 55 | if w_max >= w && h_max >= h { 56 | Some((w, h)) 57 | } else { 58 | let wf = w_max as f32 / w as f32; 59 | let hf = h_max as f32 / h as f32; 60 | let f = wf.min(hf); 61 | let (w_out, h_out) = ((w as f32 * f) as u32, (h as f32 * f) as u32); 62 | if (w_out > 0) && (h_out > 0) { 63 | Some((w_out, h_out)) 64 | } else { 65 | None 66 | } 67 | } 68 | } 69 | 70 | /// Calculates the maximum image dimensions `w` and `h` that fit inside `w_max` and `h_max` 71 | /// retaining the original aspect ratio, where both the width and height must be divisble by two. 72 | /// 73 | /// Note that this method will even reduce the dimensions to even width and height if they already 74 | /// fit in `fit_dims`. 75 | /// 76 | /// # Arguments 77 | /// 78 | /// * `dims` - Original dimensions: width and height. 79 | /// * `fit_dims` - Dimensions to fit in: width and height. 80 | /// 81 | /// # Return value 82 | /// 83 | /// The fitted dimensions if they exist and are positive and more than zero. 84 | fn calculate_fit_dims_even(dims: (u32, u32), fit_dims: (u32, u32)) -> Option<(u32, u32)> { 85 | let (w, h) = dims; 86 | let (mut w_max, mut h_max) = fit_dims; 87 | while w_max > 0 && h_max > 0 { 88 | let wf = w_max as f32 / w as f32; 89 | let hf = h_max as f32 / h as f32; 90 | let f = wf.min(hf).min(1.0); 91 | let out_w = (w as f32 * f).round() as u32; 92 | let out_h = (h as f32 * f).round() as u32; 93 | if (out_w > 0) && (out_h > 0) { 94 | if (out_w % 2 == 0) && (out_h % 2 == 0) { 95 | return Some((out_w, out_h)); 96 | } else if wf < hf { 97 | w_max -= 1; 98 | } else { 99 | h_max -= 1; 100 | } 101 | } else { 102 | break; 103 | } 104 | } 105 | None 106 | } 107 | 108 | #[cfg(test)] 109 | mod tests { 110 | use super::*; 111 | 112 | const TESTING_DIM_CANDIDATES: [u32; 8] = [0, 1, 2, 3, 8, 111, 256, 1000]; 113 | 114 | #[test] 115 | fn calculate_fit_dims_works() { 116 | let testset = generate_testset(); 117 | for ((w, h), (fit_w, fit_h)) in testset { 118 | let out = calculate_fit_dims((w, h), (fit_w, fit_h)); 119 | if let Some((out_w, out_h)) = out { 120 | let input_dim_zero = w == 0 || h == 0 || fit_w == 0 || fit_h == 0; 121 | let output_dim_zero = out_w == 0 || out_h == 0; 122 | assert!( 123 | (input_dim_zero && output_dim_zero) || (!input_dim_zero && !output_dim_zero), 124 | "computed dims are never zero unless the inputs dims were", 125 | ); 126 | assert!( 127 | (out_w <= fit_w) && (out_h <= fit_h), 128 | "computed dims fit inside provided dims", 129 | ); 130 | } 131 | } 132 | } 133 | 134 | #[test] 135 | fn calculate_fit_dims_even_works() { 136 | let testset = generate_testset(); 137 | for ((w, h), (fit_w, fit_h)) in testset { 138 | let out = calculate_fit_dims_even((w, h), (fit_w, fit_h)); 139 | if let Some((out_w, out_h)) = out { 140 | let input_dim_zero = w == 0 || h == 0 || fit_w == 0 || fit_h == 0; 141 | let output_dim_zero = out_w == 0 || out_h == 0; 142 | assert!( 143 | (input_dim_zero && output_dim_zero) || (!input_dim_zero && !output_dim_zero), 144 | "computed dims are never zero unless the inputs dims were", 145 | ); 146 | assert!( 147 | (out_w % 2 == 0) && (out_h % 2 == 0), 148 | "computed dims are even", 149 | ); 150 | assert!( 151 | (out_w <= fit_w) && (out_h <= fit_h), 152 | "computed dims fit inside provided dims", 153 | ); 154 | } 155 | } 156 | } 157 | 158 | fn generate_testset() -> Vec<((u32, u32), (u32, u32))> { 159 | let testing_dims = generate_testing_dims(); 160 | testing_dims 161 | .iter() 162 | .flat_map(|dims| testing_dims.iter().map(|fit_dims| (*dims, *fit_dims))) 163 | .collect() 164 | } 165 | 166 | fn generate_testing_dims() -> Vec<(u32, u32)> { 167 | TESTING_DIM_CANDIDATES 168 | .iter() 169 | .flat_map(|a| TESTING_DIM_CANDIDATES.iter().map(|b| (*a, *b))) 170 | .collect() 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::{CStr, CString, OsStr, OsString}; 2 | use std::os::raw::c_char; 3 | use std::path::Path; 4 | 5 | /// &Path -> &Cstr 6 | pub fn from_path + ?Sized>(path: &P) -> CString { 7 | #[cfg(unix)] 8 | { 9 | use std::os::unix::ffi::OsStrExt; 10 | CString::new(path.as_ref().as_os_str().as_bytes()).unwrap() 11 | } 12 | 13 | #[cfg(not(unix))] 14 | { 15 | use std::os::windows::ffi::OsStrExt; 16 | let wide: Vec = path.as_ref().as_os_str().encode_wide().collect(); 17 | let path_str = String::from_utf16_lossy(&wide); 18 | CString::new(path_str.as_bytes()).unwrap() 19 | } 20 | } 21 | 22 | /// Option<&Path> -> Option 23 | pub fn from_path_opt + ?Sized>(path: Option<&P>) -> Option { 24 | path.map(from_path) 25 | } 26 | 27 | /// &Cstr -> &Path 28 | /// - Unix: 使用原始字节直接构造路径(允许任意字节) 29 | /// - Windows: 假设输入为 UTF-16 LE 编码的字节序列 30 | pub fn to_path + ?Sized>(cstr: &C) -> &Path { 31 | #[cfg(unix)] 32 | { 33 | use std::os::unix::ffi::OsStrExt; 34 | Path::new(OsStr::from_bytes(cstr.as_ref().to_bytes())) 35 | } 36 | 37 | #[cfg(not(unix))] 38 | { 39 | let bytes = cstr.as_ref().to_bytes(); 40 | match std::str::from_utf8(bytes) { 41 | Ok(s) => Path::new(s), 42 | Err(_) => { 43 | // not UTF-8 44 | let os_str = unsafe { OsStr::from_encoded_bytes_unchecked(bytes) }; 45 | Path::new(os_str) 46 | } 47 | } 48 | } 49 | } 50 | 51 | /// &str -> CString 52 | pub fn from_str + ?Sized>(s: &S) -> CString { 53 | CString::new(s.as_ref()).unwrap() 54 | } 55 | 56 | /// Option<&str> -> Option 57 | pub fn from_str_opt + ?Sized>(s: Option<&S>) -> Option { 58 | s.map(from_str) 59 | } 60 | 61 | /// &Cstr -> String 62 | pub fn to_string + ?Sized>(cstr: &C) -> Result { 63 | cstr.as_ref().to_str().map(String::from) 64 | } 65 | 66 | /// OsStr -> CString 67 | pub fn from_os_str(path_or_url: impl AsRef) -> CString { 68 | #[cfg(unix)] 69 | { 70 | use std::os::unix::ffi::OsStrExt; 71 | CString::new(path_or_url.as_ref().as_bytes()).unwrap() 72 | } 73 | #[cfg(not(unix))] 74 | { 75 | CString::new(path_or_url.as_ref().to_string_lossy().as_bytes()).unwrap() 76 | } 77 | } 78 | 79 | /// CStr -> OsString 80 | pub fn to_os_string(cstr: impl AsRef) -> OsString { 81 | #[cfg(unix)] 82 | { 83 | use std::os::unix::ffi::OsStringExt; 84 | OsString::from_vec(cstr.as_ref().to_bytes().to_vec()) 85 | } 86 | #[cfg(not(unix))] 87 | { 88 | OsString::from(cstr.as_ref().to_string_lossy().into_owned()) 89 | } 90 | } 91 | 92 | /// 将 C 字符串指针转换为 Rust 字符串引用 93 | /// 94 | /// # Safety 95 | /// 96 | /// - 指针必须指向一个有效的以 null 结尾的 C 字符串 97 | /// - 字符串内容必须是有效的 UTF-8 98 | pub unsafe fn from_c_char(ptr: *const c_char) -> String { 99 | if ptr.is_null() { 100 | return String::new(); 101 | } 102 | let cstr = CStr::from_ptr(ptr); 103 | match cstr.to_str() { 104 | Ok(s) => s.to_owned(), 105 | Err(_) => { 106 | // NOT UTF-8 107 | cstr.to_string_lossy().into_owned() 108 | } 109 | } 110 | } 111 | 112 | /// 将 Rust 字符串转换为 C 字符串指针 113 | /// 114 | /// # Safety 115 | /// 116 | /// 返回的指针需要手动释放,否则会造成内存泄漏 117 | /// 使用 `free_cstr` 函数释放内存 118 | pub unsafe fn to_c_char(s: &str) -> *mut c_char { 119 | CString::new(s).unwrap().into_raw() 120 | } 121 | 122 | #[cfg(test)] 123 | mod tests { 124 | use super::*; 125 | use std::path::Path; 126 | use std::path::PathBuf; 127 | 128 | #[test] 129 | fn test_path_conversion() { 130 | // 使用平台无关的路径分隔符 131 | let path_str = if cfg!(unix) { 132 | "/usr/local/bin" 133 | } else { 134 | r"C:\Users\local\bin" 135 | }; 136 | 137 | // 从 &str 路径 138 | let cstring = from_path(Path::new(path_str)); 139 | assert_eq!(cstring.to_str().unwrap(), path_str); 140 | 141 | // 从 PathBuf 142 | let path_buf = PathBuf::from(path_str); 143 | let cstring = from_path(&path_buf); 144 | assert_eq!(cstring.to_str().unwrap(), path_str); 145 | 146 | // UTF-8 中文路径 (使用平台特定分隔符) 147 | let chinese_path = if cfg!(unix) { 148 | "测试/文件.txt" 149 | } else { 150 | r"测试\文件.txt" 151 | }; 152 | let chinese = CString::new(chinese_path).unwrap(); 153 | let utf8_path = to_path(&chinese); 154 | assert_eq!(utf8_path.to_str().unwrap(), chinese_path); 155 | 156 | #[cfg(unix)] 157 | { 158 | // NOT UTF-8 159 | use std::os::unix::ffi::OsStrExt; 160 | let gbk = CString::new(vec![0xB2, 0xE2, 0xCA, 0xD4, 0x2E, 0x74, 0x78, 0x74]).unwrap(); 161 | let gbk_path = to_path(&gbk); 162 | assert_eq!( 163 | gbk_path.as_os_str().as_bytes(), 164 | &[0xB2, 0xE2, 0xCA, 0xD4, 0x2E, 0x74, 0x78, 0x74] 165 | ); 166 | } 167 | 168 | #[cfg(not(unix))] 169 | { 170 | use std::os::windows::ffi::{OsStrExt, OsStringExt}; 171 | // Windows 下使用 UTF-16 测试 172 | let test_str = "测试.txt"; 173 | let path = Path::new(test_str); 174 | let os_str = path.as_os_str(); 175 | let wide_chars: Vec = os_str.encode_wide().collect(); 176 | let os_string = OsString::from_wide(&wide_chars); 177 | let cstring = from_path(&os_string); 178 | let result_path = to_path(&cstring); 179 | assert_eq!(result_path.to_str().unwrap(), test_str); 180 | } 181 | } 182 | 183 | #[test] 184 | fn test_str_conversion() { 185 | // 从 &str 186 | let s = "hello world"; 187 | let cstring = from_str(s); 188 | assert_eq!(cstring.to_str().unwrap(), s); 189 | 190 | // 从 String 191 | let string = String::from("hello world"); 192 | let cstring = from_str(&string); 193 | assert_eq!(cstring.to_str().unwrap(), string); 194 | 195 | // 从 &OsStr 196 | let os_str = "/usr/local/bin"; 197 | let cstring = from_os_str(os_str); 198 | assert_eq!(cstring.to_str().unwrap(), os_str); 199 | 200 | // to OsString 201 | let cstring = CString::new(os_str).unwrap(); 202 | let os_string = to_os_string(cstring); 203 | assert_eq!(os_string.to_str().unwrap(), os_str); 204 | } 205 | 206 | #[test] 207 | fn test_optional_conversion() { 208 | // 使用平台特定的路径 209 | let test_path = if cfg!(unix) { 210 | "/usr/local" 211 | } else { 212 | r"C:\Users\local" 213 | }; 214 | 215 | // Optional Path 216 | let path: Option<&Path> = Some(Path::new(test_path)); 217 | let cstring = from_path_opt(path); 218 | assert!(cstring.is_some()); 219 | assert_eq!(cstring.unwrap().to_str().unwrap(), test_path); 220 | 221 | // Optional str 222 | let s: Option<&str> = Some("hello"); 223 | let cstring = from_str_opt(s); 224 | assert!(cstring.is_some()); 225 | assert_eq!(cstring.unwrap().to_str().unwrap(), "hello"); 226 | 227 | // None cases 228 | let none_path: Option<&Path> = None; 229 | assert!(from_path_opt(none_path).is_none()); 230 | 231 | let none_str: Option<&str> = None; 232 | assert!(from_str_opt(none_str).is_none()); 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /tests/decode_audio.rs: -------------------------------------------------------------------------------- 1 | //! RIIR: https://github.com/FFmpeg/FFmpeg/blob/master/doc/examples/decode_audio.c 2 | use anyhow::{anyhow, Context, Result}; 3 | use rsmpeg::{ 4 | avcodec::{AVCodecContext, AVCodecParserContext, AVPacket}, 5 | avformat::AVFormatContextInput, 6 | avutil::{ 7 | get_bytes_per_sample, get_packed_sample_fmt, get_sample_fmt_name, sample_fmt_is_planar, 8 | AVFrame, AVSampleFormat, 9 | }, 10 | error::RsmpegError, 11 | ffi, 12 | }; 13 | use std::{ 14 | ffi::CString, 15 | fs::{self, File}, 16 | io::{Read, Write}, 17 | path::Path, 18 | slice::from_raw_parts, 19 | }; 20 | 21 | fn get_format_from_sample_fmt(sample_fmt: AVSampleFormat) -> Option<&'static str> { 22 | let sample_fmt_entries = [ 23 | (ffi::AV_SAMPLE_FMT_U8, "u8"), 24 | (ffi::AV_SAMPLE_FMT_S16, "s16le"), 25 | (ffi::AV_SAMPLE_FMT_S32, "s32le"), 26 | (ffi::AV_SAMPLE_FMT_FLT, "f32le"), 27 | (ffi::AV_SAMPLE_FMT_DBL, "f64le"), 28 | ]; 29 | sample_fmt_entries 30 | .iter() 31 | .find(|(fmt, _)| *fmt == sample_fmt) 32 | .map(|(_, fmt)| *fmt) 33 | } 34 | 35 | fn frame_save(frame: &AVFrame, channels: usize, data_size: usize, mut file: &File) -> Result<()> { 36 | let nb_samples: usize = frame.nb_samples.try_into().context("nb_samples overflow")?; 37 | // ATTENTION: This is only valid for planar sample formats. 38 | for i in 0..nb_samples { 39 | for channel in 0..channels { 40 | let data = unsafe { from_raw_parts(frame.data[channel].add(data_size * i), data_size) }; 41 | file.write_all(data).context("Write data failed.")?; 42 | } 43 | } 44 | Ok(()) 45 | } 46 | 47 | fn decode( 48 | decode_context: &mut AVCodecContext, 49 | packet: Option<&AVPacket>, 50 | out_file: &File, 51 | ) -> Result<()> { 52 | decode_context 53 | .send_packet(packet) 54 | .context("Send packet failed.")?; 55 | let channels = decode_context 56 | .ch_layout 57 | .nb_channels 58 | .try_into() 59 | .context("channels overflow")?; 60 | let sample_fmt = decode_context.sample_fmt; 61 | loop { 62 | let frame = match decode_context.receive_frame() { 63 | Ok(frame) => frame, 64 | Err(RsmpegError::DecoderDrainError) | Err(RsmpegError::DecoderFlushedError) => break, 65 | Err(e) => return Err(e).context("Receive frame failed."), 66 | }; 67 | let data_size = get_bytes_per_sample(sample_fmt).context("Unknown sample fmt")?; 68 | frame_save(&frame, channels, data_size, out_file)?; 69 | } 70 | Ok(()) 71 | } 72 | 73 | fn decode_audio(audio_path: &str, out_file_path: &str) -> Result<()> { 74 | const AUDIO_INBUF_SIZE: usize = 20480; 75 | 76 | let (decoder, mut decode_context) = { 77 | // safety, &str ensures no internal null bytes. 78 | let audio_path = CString::new(audio_path).unwrap(); 79 | let mut input_format_context = AVFormatContextInput::open(&audio_path, None, &mut None) 80 | .context("Open audio file failed.")?; 81 | let (stream_index, decoder) = input_format_context 82 | .find_best_stream(ffi::AVMEDIA_TYPE_AUDIO) 83 | .context("Find best stream failed.")? 84 | .context("Cannot find audio stream in this file.")?; 85 | let mut decode_context = AVCodecContext::new(&decoder); 86 | decode_context 87 | .apply_codecpar(&input_format_context.streams()[stream_index].codecpar()) 88 | .context("Apply codecpar failed.")?; 89 | decode_context.open(None).context("Could not open codec")?; 90 | input_format_context.dump(stream_index, &audio_path)?; 91 | (decoder, decode_context) 92 | }; 93 | 94 | let mut inbuf = [0u8; AUDIO_INBUF_SIZE + ffi::AV_INPUT_BUFFER_PADDING_SIZE as usize]; 95 | 96 | let mut audio_file = 97 | File::open(audio_path).with_context(|| anyhow!("Could not open {}", audio_path))?; 98 | fs::create_dir_all(Path::new(out_file_path).parent().unwrap()).unwrap(); 99 | let out_file = File::create(out_file_path).context("Open out file failed.")?; 100 | 101 | let mut parser_context = AVCodecParserContext::init(decoder.id).context("Parser not found")?; 102 | let mut packet = AVPacket::new(); 103 | 104 | loop { 105 | let len = audio_file 106 | .read(&mut inbuf[..AUDIO_INBUF_SIZE]) 107 | .context("Read input file failed.")?; 108 | if len == 0 { 109 | break; 110 | } 111 | let mut parsed_offset = 0; 112 | while parsed_offset < len { 113 | let (get_packet, offset) = parser_context 114 | .parse_packet(&mut decode_context, &mut packet, &inbuf[parsed_offset..len]) 115 | .context("Error while parsing")?; 116 | parsed_offset += offset; 117 | if get_packet { 118 | decode(&mut decode_context, Some(&packet), &out_file)?; 119 | } 120 | } 121 | } 122 | 123 | // Flush parser 124 | let (get_packet, _) = parser_context 125 | .parse_packet(&mut decode_context, &mut packet, &[]) 126 | .context("Error while parsing")?; 127 | if get_packet { 128 | decode(&mut decode_context, Some(&packet), &out_file)?; 129 | } 130 | 131 | // Flush decoder 132 | decode(&mut decode_context, None, &out_file)?; 133 | 134 | let mut sample_fmt = decode_context.sample_fmt; 135 | 136 | if sample_fmt_is_planar(sample_fmt) { 137 | let name = get_sample_fmt_name(sample_fmt); 138 | println!( 139 | "Warning: the sample format the decoder produced is planar \ 140 | ({}). This example will output the first channel only.", 141 | name.map(|x| x.to_str().unwrap()).unwrap_or("?") 142 | ); 143 | sample_fmt = get_packed_sample_fmt(sample_fmt).context("Cannot get packed sample fmt")?; 144 | } 145 | 146 | let fmt = get_format_from_sample_fmt(sample_fmt).context("Unsupported sample fmt")?; 147 | 148 | println!("Play the output audio file with the command:"); 149 | println!( 150 | "ffplay -f {} -ac {} -ar {} {}", 151 | fmt, decode_context.ch_layout.nb_channels, decode_context.sample_rate, out_file_path 152 | ); 153 | Ok(()) 154 | } 155 | 156 | #[test] 157 | #[ignore = "decode_audio_test 测试运行依赖测试文件,暂时忽略"] 158 | fn decode_audio_test() { 159 | decode_audio( 160 | "tests/assets/audios/sample1_short.aac", 161 | "tests/output/decode_audio/sample1_short.pcm", 162 | ) 163 | .unwrap(); 164 | } 165 | -------------------------------------------------------------------------------- /tests/decode_video.rs: -------------------------------------------------------------------------------- 1 | //! RIIR: https://github.com/FFmpeg/FFmpeg/blob/master/doc/examples/decode_video.c 2 | use anyhow::{anyhow, Context, Result}; 3 | use camino::Utf8Path as Path; 4 | use rsmpeg::{ 5 | avcodec::{AVCodec, AVCodecContext, AVCodecParserContext, AVPacket}, 6 | avutil::AVFrame, 7 | error::RsmpegError, 8 | ffi, 9 | }; 10 | use std::{ 11 | fs::{self, File}, 12 | io::prelude::*, 13 | slice, 14 | }; 15 | 16 | /// Save a `AVFrame` as pgm file. 17 | fn pgm_save(frame: &AVFrame, filename: &str) -> Result<()> { 18 | // Here we only capture the first layer of frame. 19 | let data = frame.data[0]; 20 | let linesize = frame.linesize[0] as usize; 21 | 22 | let width = frame.width as usize; 23 | let height = frame.height as usize; 24 | 25 | let buffer = unsafe { slice::from_raw_parts(data, linesize * height) }; 26 | 27 | // Create pgm file 28 | let mut pgm_file = fs::File::create(filename)?; 29 | 30 | // Write pgm header 31 | pgm_file.write_all(&format!("P5\n{} {}\n{}\n", width, height, 255).into_bytes())?; 32 | 33 | // Write pgm data 34 | for i in 0..height { 35 | pgm_file.write_all(&buffer[i * linesize..i * linesize + width])?; 36 | } 37 | 38 | pgm_file.flush()?; 39 | 40 | Ok(()) 41 | } 42 | 43 | /// Push packet to `decode_context`, then save the output frames(fetched from the 44 | /// `decode_context`) as pgm files. 45 | fn decode( 46 | decode_context: &mut AVCodecContext, 47 | packet: Option<&AVPacket>, 48 | out_dir: &str, 49 | out_filename: &str, 50 | ) -> Result<()> { 51 | decode_context.send_packet(packet)?; 52 | loop { 53 | let frame = match decode_context.receive_frame() { 54 | Ok(frame) => frame, 55 | Err(RsmpegError::DecoderDrainError) | Err(RsmpegError::DecoderFlushedError) => break, 56 | Err(e) => Err(e).context("Error during decoding")?, 57 | }; 58 | println!("saving frame {}", decode_context.frame_num); 59 | pgm_save( 60 | &frame, 61 | &format!( 62 | "{}/{}-{}.pgm", 63 | out_dir, out_filename, decode_context.frame_num 64 | ), 65 | )?; 66 | } 67 | Ok(()) 68 | } 69 | 70 | /// This function extracts frames from a MPEG1 video, then save them to `out_dir` as pgm. 71 | fn decode_video(video_path: &str, out_dir: &str) -> Result<()> { 72 | const INBUF_SIZE: usize = 4096; 73 | let video_path = Path::new(video_path); 74 | let out_filename = video_path.file_stem().unwrap(); 75 | fs::create_dir_all(out_dir).unwrap(); 76 | 77 | // set end of buffer to 0 (this ensures that no overreading happens for damaged MPEG streams) 78 | let mut inbuf = vec![0u8; INBUF_SIZE + ffi::AV_INPUT_BUFFER_PADDING_SIZE as usize]; 79 | 80 | let decoder = AVCodec::find_decoder(ffi::AV_CODEC_ID_MPEG1VIDEO).context("Codec not found")?; 81 | let mut decode_context = AVCodecContext::new(&decoder); 82 | decode_context.open(None).context("Could not open codec")?; 83 | 84 | let mut video_file = 85 | File::open(video_path).with_context(|| anyhow!("Could not open {}", video_path))?; 86 | 87 | let mut parser_context = AVCodecParserContext::init(decoder.id).context("Parser not found")?; 88 | let mut packet = AVPacket::new(); 89 | 90 | loop { 91 | let len = video_file 92 | .read(&mut inbuf[..INBUF_SIZE]) 93 | .context("Read input file failed.")?; 94 | if len == 0 { 95 | break; 96 | } 97 | let mut parsed_offset = 0; 98 | while parsed_offset < len { 99 | let (get_packet, offset) = parser_context 100 | .parse_packet(&mut decode_context, &mut packet, &inbuf[parsed_offset..len]) 101 | .context("Error while parsing")?; 102 | parsed_offset += offset; 103 | if get_packet { 104 | decode(&mut decode_context, Some(&packet), out_dir, out_filename)?; 105 | } 106 | } 107 | } 108 | 109 | // Flush parser 110 | let (get_packet, _) = parser_context 111 | .parse_packet(&mut decode_context, &mut packet, &[]) 112 | .context("Error while parsing")?; 113 | if get_packet { 114 | decode(&mut decode_context, Some(&packet), out_dir, out_filename)?; 115 | } 116 | 117 | // Flush decoder 118 | decode(&mut decode_context, None, out_dir, out_filename)?; 119 | 120 | Ok(()) 121 | } 122 | 123 | #[test] 124 | #[ignore = "decode_video_test 测试运行依赖测试文件,暂时忽略"] 125 | fn decode_video_test() { 126 | decode_video("tests/assets/vids/centaur.mpg", "tests/output/decode_video").unwrap(); 127 | } 128 | -------------------------------------------------------------------------------- /tests/encode_video.rs: -------------------------------------------------------------------------------- 1 | //! RIIR: https://github.com/FFmpeg/FFmpeg/blob/master/doc/examples/encode_video.c 2 | use anyhow::{anyhow, Context, Result}; 3 | use rsmpeg::{ 4 | avcodec::{AVCodec, AVCodecContext}, 5 | avutil::{opt_set, ra, AVFrame}, 6 | error::RsmpegError, 7 | ffi, 8 | }; 9 | use std::{ 10 | ffi::CStr, 11 | fs::{self, File}, 12 | io::{BufWriter, Write}, 13 | }; 14 | 15 | const WIDTH: usize = 352; 16 | const HEIGHT: usize = 288; 17 | 18 | fn encode( 19 | encode_context: &mut AVCodecContext, 20 | frame: Option<&AVFrame>, 21 | file: &mut BufWriter, 22 | ) -> Result<()> { 23 | encode_context.send_frame(frame)?; 24 | loop { 25 | let packet = match encode_context.receive_packet() { 26 | Ok(packet) => packet, 27 | Err(RsmpegError::EncoderDrainError) | Err(RsmpegError::EncoderFlushedError) => break, 28 | Err(e) => return Err(e.into()), 29 | }; 30 | let data = unsafe { std::slice::from_raw_parts(packet.data, packet.size as usize) }; 31 | file.write_all(data)?; 32 | } 33 | Ok(()) 34 | } 35 | 36 | fn encode_video(codec_name: &CStr, file_name: &str) -> Result<()> { 37 | let encoder = 38 | AVCodec::find_encoder_by_name(codec_name).context("Failed to find encoder codec")?; 39 | let mut encode_context = AVCodecContext::new(&encoder); 40 | encode_context.set_bit_rate(400000); 41 | encode_context.set_width(WIDTH as i32); 42 | encode_context.set_height(HEIGHT as i32); 43 | encode_context.set_time_base(ra(1, 25)); 44 | encode_context.set_framerate(ra(25, 1)); 45 | encode_context.set_gop_size(10); 46 | encode_context.set_max_b_frames(1); 47 | encode_context.set_pix_fmt(ffi::AV_PIX_FMT_YUV420P); 48 | if encoder.id == ffi::AV_CODEC_ID_H264 { 49 | unsafe { opt_set(encode_context.priv_data, c"preset", c"slow", 0) } 50 | .context("Set preset failed.")?; 51 | } 52 | encode_context.open(None).context("Could not open codec")?; 53 | 54 | let mut frame = AVFrame::new(); 55 | frame.set_format(encode_context.pix_fmt); 56 | frame.set_width(encode_context.width); 57 | frame.set_height(encode_context.height); 58 | frame 59 | .alloc_buffer() 60 | .context("Could not allocate the video frame data")?; 61 | 62 | let file = File::create(file_name).with_context(|| anyhow!("Could not open: {}", file_name))?; 63 | let mut writer = BufWriter::new(file); 64 | 65 | for i in 0..25 { 66 | frame 67 | .make_writable() 68 | .context("Failed to make frame writable")?; 69 | // prepare colorful frame 70 | { 71 | let data = frame.data; 72 | let linesize = frame.linesize; 73 | let linesize_y = linesize[0] as usize; 74 | let linesize_cb = linesize[1] as usize; 75 | let linesize_cr = linesize[2] as usize; 76 | let y_data = unsafe { std::slice::from_raw_parts_mut(data[0], HEIGHT * linesize_y) }; 77 | let cb_data = 78 | unsafe { std::slice::from_raw_parts_mut(data[1], HEIGHT / 2 * linesize_cb) }; 79 | let cr_data = 80 | unsafe { std::slice::from_raw_parts_mut(data[2], HEIGHT / 2 * linesize_cr) }; 81 | // prepare a dummy image 82 | for y in 0..HEIGHT { 83 | for x in 0..WIDTH { 84 | y_data[y * linesize_y + x] = (x + y + i * 3) as u8; 85 | } 86 | } 87 | 88 | for y in 0..HEIGHT / 2 { 89 | for x in 0..WIDTH / 2 { 90 | cb_data[y * linesize_cb + x] = (128 + y + i * 2) as u8; 91 | cr_data[y * linesize_cr + x] = (64 + x + i * 5) as u8; 92 | } 93 | } 94 | } 95 | 96 | frame.set_pts(i as i64); 97 | 98 | encode(&mut encode_context, Some(&frame), &mut writer)?; 99 | } 100 | encode(&mut encode_context, None, &mut writer)?; 101 | 102 | let endcode: [u8; 4] = [0, 0, 1, 0xb7]; 103 | writer.write_all(&endcode).context("Write endcode failed")?; 104 | 105 | writer.flush().context("Flush file failed.") 106 | } 107 | 108 | #[test] 109 | #[ignore = "encode_video_test 测试运行依赖测试文件,暂时忽略"] 110 | fn encode_video_test() { 111 | fs::create_dir_all("tests/output/encode_video/").unwrap(); 112 | encode_video(c"mpeg4", "tests/output/encode_video/output.mp4").unwrap(); 113 | } 114 | -------------------------------------------------------------------------------- /tests/extract_mvs.rs: -------------------------------------------------------------------------------- 1 | //! RIIR: https://github.com/FFmpeg/FFmpeg/blob/master/doc/examples/extract_mvs.c 2 | use anyhow::{anyhow, Context, Result}; 3 | use rsmpeg::{ 4 | avcodec::{AVCodecContext, AVPacket}, 5 | avformat::AVFormatContextInput, 6 | avutil::{get_media_type_string, AVDictionary}, 7 | error::RsmpegError, 8 | ffi, 9 | }; 10 | use std::ffi::{CStr, CString}; 11 | 12 | fn decode_packet( 13 | decode_context: &mut AVCodecContext, 14 | packet: Option<&AVPacket>, 15 | video_frame_count: &mut usize, 16 | ) -> Result<()> { 17 | decode_context 18 | .send_packet(packet) 19 | .context("Error while sending a packet to the decoder")?; 20 | 21 | loop { 22 | let frame = match decode_context.receive_frame() { 23 | Ok(frame) => frame, 24 | Err(RsmpegError::DecoderDrainError) | Err(RsmpegError::DecoderFlushedError) => break, 25 | Err(e) => return Err(e.into()), 26 | }; 27 | 28 | *video_frame_count += 1; 29 | 30 | if let Some(side_data) = frame.get_side_data(ffi::AV_FRAME_DATA_MOTION_VECTORS) { 31 | let raw_motion_vectors = unsafe { side_data.as_motion_vectors() }; 32 | for &motion_vector in raw_motion_vectors { 33 | println!( 34 | "{},{:2},{:2},{:2},{:4},{:4},{:4},{:4},{:#x},{:4},{:4},{:4}", 35 | video_frame_count, 36 | motion_vector.source, 37 | motion_vector.w, 38 | motion_vector.h, 39 | motion_vector.src_x, 40 | motion_vector.src_y, 41 | motion_vector.dst_x, 42 | motion_vector.dst_y, 43 | motion_vector.flags, 44 | motion_vector.motion_x, 45 | motion_vector.motion_y, 46 | motion_vector.motion_scale, 47 | ); 48 | } 49 | }; 50 | } 51 | Ok(()) 52 | } 53 | 54 | /// Extract motion vectors from a video. 55 | fn extract_mvs(video_path: &CStr) -> Result<()> { 56 | let mut input_format_context = AVFormatContextInput::open(video_path, None, &mut None)?; 57 | let media_type = ffi::AVMEDIA_TYPE_VIDEO; 58 | 59 | let (stream_index, mut decode_context) = { 60 | let (stream_index, decoder) = input_format_context 61 | .find_best_stream(media_type)? 62 | .with_context(|| { 63 | anyhow!( 64 | "Could not find {} stream in input file '{}'", 65 | get_media_type_string(media_type).unwrap().to_string_lossy(), 66 | video_path.to_string_lossy() 67 | ) 68 | })?; 69 | 70 | let stream = &input_format_context.streams()[stream_index]; 71 | 72 | let mut decode_context = AVCodecContext::new(&decoder); 73 | 74 | decode_context 75 | .apply_codecpar(&stream.codecpar()) 76 | .context("Failed to copy codec parameters to codec context")?; 77 | 78 | let opts = AVDictionary::new(c"flags2", c"+export_mvs", 0); 79 | 80 | decode_context.open(Some(opts)).with_context(|| { 81 | anyhow!( 82 | "Failed to open {} codec", 83 | get_media_type_string(media_type).unwrap().to_string_lossy() 84 | ) 85 | })?; 86 | 87 | (stream_index, decode_context) 88 | }; 89 | 90 | input_format_context 91 | .dump(0, video_path) 92 | .context("Input format context dump failed.")?; 93 | 94 | println!( 95 | "framenum,source,blockw,blockh,srcx,srcy,dstx,dsty,flags,motion_x,motion_y,motion_scale" 96 | ); 97 | 98 | let mut video_frame_count = 0; 99 | 100 | while let Some(packet) = input_format_context.read_packet().unwrap() { 101 | if packet.stream_index == stream_index as i32 { 102 | decode_packet(&mut decode_context, Some(&packet), &mut video_frame_count)?; 103 | } 104 | } 105 | 106 | decode_packet(&mut decode_context, None, &mut video_frame_count)?; 107 | 108 | Ok(()) 109 | } 110 | 111 | #[test] 112 | #[ignore = "extract_mvs_test 测试运行依赖测试文件,暂时忽略"] 113 | fn extract_mvs_test() { 114 | let video_path = &CString::new("tests/assets/vids/bear.mp4").unwrap(); 115 | extract_mvs(video_path).unwrap(); 116 | } 117 | -------------------------------------------------------------------------------- /tests/remux.rs: -------------------------------------------------------------------------------- 1 | //! RIIR: https://github.com/FFmpeg/FFmpeg/blob/master/doc/examples/remux.c 2 | use anyhow::{Context, Result}; 3 | use rsmpeg::{ 4 | avcodec::AVPacket, 5 | avformat::{AVFormatContextInput, AVFormatContextOutput}, 6 | avutil::{ts2str, ts2timestr}, 7 | ffi::AVRational, 8 | }; 9 | use std::ffi::CStr; 10 | 11 | fn log_packet(time_base: AVRational, pkt: &AVPacket, tag: &str) { 12 | println!( 13 | "{}: pts:{} pts_time:{} dts:{} dts_time:{} duration:{} duration_time:{} stream_index:{}", 14 | tag, 15 | ts2str(pkt.pts), 16 | ts2timestr(pkt.pts, time_base), 17 | ts2str(pkt.dts), 18 | ts2timestr(pkt.dts, time_base), 19 | ts2str(pkt.duration), 20 | ts2timestr(pkt.duration, time_base), 21 | pkt.stream_index 22 | ); 23 | } 24 | 25 | fn remux(input_path: &CStr, output_path: &CStr) -> Result<()> { 26 | let mut input_format_context = AVFormatContextInput::open(input_path, None, &mut None) 27 | .context("Create input format context failed.")?; 28 | input_format_context 29 | .dump(0, input_path) 30 | .context("Dump input format context failed.")?; 31 | let mut output_format_context = AVFormatContextOutput::create(output_path, None) 32 | .context("Create output format context failed.")?; 33 | let stream_mapping: Vec<_> = { 34 | let mut stream_index = 0usize; 35 | input_format_context 36 | .streams() 37 | .into_iter() 38 | .map(|stream| { 39 | let codec_type = stream.codecpar().codec_type(); 40 | if !codec_type.is_video() && !codec_type.is_audio() && !codec_type.is_subtitle() { 41 | None 42 | } else { 43 | output_format_context 44 | .new_stream() 45 | .set_codecpar(stream.codecpar().clone()); 46 | stream_index += 1; 47 | Some(stream_index - 1) 48 | } 49 | }) 50 | .collect() 51 | }; 52 | output_format_context 53 | .dump(0, output_path) 54 | .context("Dump output format context failed.")?; 55 | 56 | output_format_context 57 | .write_header(&mut None) 58 | .context("Writer header failed.")?; 59 | 60 | while let Some(mut packet) = input_format_context 61 | .read_packet() 62 | .context("Read packet failed.")? 63 | { 64 | let input_stream_index = packet.stream_index as usize; 65 | let Some(output_stream_index) = stream_mapping[input_stream_index] else { 66 | continue; 67 | }; 68 | { 69 | let input_stream = &input_format_context.streams()[input_stream_index]; 70 | let output_stream = &output_format_context.streams()[output_stream_index]; 71 | log_packet(input_stream.time_base, &packet, "in"); 72 | packet.rescale_ts(input_stream.time_base, output_stream.time_base); 73 | packet.set_stream_index(output_stream_index as i32); 74 | packet.set_pos(-1); 75 | log_packet(output_stream.time_base, &packet, "out"); 76 | } 77 | output_format_context 78 | .interleaved_write_frame(&mut packet) 79 | .context("Interleaved write frame failed.")?; 80 | } 81 | output_format_context 82 | .write_trailer() 83 | .context("Write trailer failed.") 84 | } 85 | 86 | #[test] 87 | #[ignore = "Remux MP4 to MOV, with h.264 codec 测试运行依赖测试文件,暂时忽略"] 88 | fn remux_test0() { 89 | std::fs::create_dir_all("tests/output/remux/").unwrap(); 90 | remux( 91 | c"tests/assets/vids/big_buck_bunny.mp4", 92 | c"tests/output/remux/big_buck_bunny.mov", 93 | ) 94 | .unwrap(); 95 | } 96 | -------------------------------------------------------------------------------- /tests/string_test.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod tests { 3 | #[test] 4 | fn string_test() { 5 | // 1. 普通字符串字面量 (String literals) 6 | println!( 7 | "1. &str: {:?}, {:?}, {:?}, {:?}", 8 | 'R', "foo", r"foo", r#"foo "bar" baz"# 9 | ); 10 | assert_eq!("foo", r"foo"); 11 | assert_eq!("R", r"R"); 12 | assert_eq!("\x52", "R"); 13 | assert_eq!("foo \"bar\" baz", "foo \"bar\" baz"); 14 | assert_eq!("foo \"bar\" baz", r#"foo "bar" baz"#); 15 | assert_eq!("foo #\"# bar", r##"foo #"# bar"##); 16 | 17 | // 2. 字节字符串字面量 (Byte string literals) 18 | println!( 19 | "2. Byte string &[u8]: {:?}, {:?}, {:?}, {:?}", 20 | b'R', b"foo", br"foo", br#"foo "bar" baz"# 21 | ); 22 | assert_eq!(b'R', 82_u8); 23 | assert_eq!(br"foo", b"foo"); 24 | assert_eq!(b"foo \"bar\" baz", br#"foo "bar" baz"#); 25 | assert_eq!(b"foo #\"# bar", br##"foo #"# bar"##); 26 | assert_eq!(b"R", br"R"); 27 | assert_eq!(b"\x52", b"R"); 28 | assert_eq!(b"\\x52", br"\x52"); 29 | 30 | // 3. C字符串字面量 (C string literals) 31 | println!("3. &CStr: {:?}, {:?}, {:?}", c"foo", cr"foo", cr#""foo""#); 32 | assert_eq!(c"foo", cr"foo"); 33 | assert_eq!(c"foo \"bar\" baz", cr#"foo "bar" baz"#); 34 | assert_eq!(c"\x52", c"R"); 35 | assert_eq!(c"\x52", cr"R"); 36 | assert_eq!(c"\\x52", cr"\x52"); 37 | 38 | // C字符串字面量自动添加 null 终止符 39 | assert_eq!(c"foo".to_bytes(), b"foo"); 40 | assert_eq!(c"foo".to_bytes_with_nul(), b"foo\0"); 41 | 42 | // 原始C字符串字面量不处理转义字符 43 | assert_eq!(cr"foo\0".to_bytes(), b"foo\\0"); 44 | assert_eq!(cr"foo\0".to_bytes_with_nul(), b"foo\\0\0"); 45 | 46 | // 带引号的C字符串示例 47 | assert_eq!(cr#""foo""#.to_bytes(), b"\"foo\""); 48 | assert_eq!(cr#""foo""#.to_bytes_with_nul(), b"\"foo\"\0"); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tests/transcode_aac.rs: -------------------------------------------------------------------------------- 1 | //! RIIR: https://github.com/FFmpeg/FFmpeg/blob/master/doc/examples/transcode_aac.c 2 | use anyhow::{bail, Context as AnyhowContext, Result}; 3 | use rsmpeg::{ 4 | avcodec::{AVCodec, AVCodecContext}, 5 | avformat::{AVFormatContextInput, AVFormatContextOutput}, 6 | avutil::{ra, AVAudioFifo, AVChannelLayout, AVFrame, AVSamples}, 7 | error::RsmpegError, 8 | ffi, 9 | swresample::SwrContext, 10 | }; 11 | use std::{ 12 | ffi::CStr, 13 | sync::atomic::{AtomicI64, Ordering}, 14 | }; 15 | 16 | /// The output bit rate in bit/s 17 | const OUTPUT_BIT_RATE: i64 = 96000; 18 | /// The number of output channels 19 | const OUTPUT_CHANNELS: i32 = 2; 20 | 21 | fn open_input_file(input_file: &CStr) -> Result<(AVFormatContextInput, AVCodecContext, usize)> { 22 | let input_format_context = AVFormatContextInput::open(input_file, None, &mut None) 23 | .context("Could not open input file")?; 24 | let (audio_index, decoder) = input_format_context 25 | .find_best_stream(ffi::AVMEDIA_TYPE_AUDIO)? 26 | .context("Failed to find audio stream")?; 27 | 28 | let stream = &input_format_context.streams()[audio_index]; 29 | let mut decode_context = AVCodecContext::new(&decoder); 30 | decode_context.apply_codecpar(&stream.codecpar())?; 31 | decode_context 32 | .open(None) 33 | .context("Could not open input codec")?; 34 | decode_context.set_pkt_timebase(stream.time_base); 35 | Ok((input_format_context, decode_context, audio_index)) 36 | } 37 | 38 | fn open_output_file( 39 | output_file: &CStr, 40 | decode_context: &AVCodecContext, 41 | ) -> Result<(AVFormatContextOutput, AVCodecContext)> { 42 | // Create a new format context for the output container format. 43 | let mut output_format_context = 44 | AVFormatContextOutput::create(output_file, None).context("Failed to open output file.")?; 45 | 46 | // Find the encoder to be used by its name. 47 | let encode_codec = 48 | AVCodec::find_encoder(ffi::AV_CODEC_ID_AAC).context("Failed to find aac encoder")?; 49 | 50 | let mut encode_context = AVCodecContext::new(&encode_codec); 51 | 52 | // Set the basic encoder parameters. 53 | // The input file's sample rate is used to avoid a sample rate conversion. 54 | encode_context.set_ch_layout(AVChannelLayout::from_nb_channels(OUTPUT_CHANNELS).into_inner()); 55 | encode_context.set_sample_rate(decode_context.sample_rate); 56 | encode_context.set_sample_fmt(encode_codec.sample_fmts().unwrap()[0]); 57 | encode_context.set_bit_rate(OUTPUT_BIT_RATE); 58 | 59 | // Open the encoder for the audio stream to use it later. 60 | encode_context.open(None)?; 61 | 62 | { 63 | // Create a new audio stream in the output file container. 64 | let mut stream = output_format_context.new_stream(); 65 | stream.set_codecpar(encode_context.extract_codecpar()); 66 | // Set the sample rate for the container. 67 | stream.set_time_base(ra(1, decode_context.sample_rate)); 68 | } 69 | 70 | Ok((output_format_context, encode_context)) 71 | } 72 | 73 | fn init_resampler( 74 | decode_context: &mut AVCodecContext, 75 | encode_context: &mut AVCodecContext, 76 | ) -> Result { 77 | let mut resample_context = SwrContext::new( 78 | &encode_context.ch_layout, 79 | encode_context.sample_fmt, 80 | encode_context.sample_rate, 81 | &decode_context.ch_layout, 82 | decode_context.sample_fmt, 83 | decode_context.sample_rate, 84 | ) 85 | .context("Could not allocate resample context")?; 86 | resample_context 87 | .init() 88 | .context("Could not open resample context")?; 89 | Ok(resample_context) 90 | } 91 | 92 | fn add_samples_to_fifo( 93 | fifo: &mut AVAudioFifo, 94 | samples_buffer: &AVSamples, 95 | frame_size: i32, 96 | ) -> Result<()> { 97 | fifo.realloc(fifo.size() + frame_size); 98 | unsafe { fifo.write(samples_buffer.audio_data.as_ptr(), frame_size) } 99 | .context("Could not write data to FIFO")?; 100 | Ok(()) 101 | } 102 | 103 | fn init_output_frame( 104 | nb_samples: i32, 105 | ch_layout: ffi::AVChannelLayout, 106 | sample_fmt: i32, 107 | sample_rate: i32, 108 | ) -> Result { 109 | let mut frame = AVFrame::new(); 110 | frame.set_nb_samples(nb_samples); 111 | frame.set_ch_layout(ch_layout); 112 | frame.set_format(sample_fmt); 113 | frame.set_sample_rate(sample_rate); 114 | 115 | frame 116 | .get_buffer(0) 117 | .context("Could not allocate output frame samples")?; 118 | 119 | Ok(frame) 120 | } 121 | 122 | /// Return boolean: if data is written. 123 | fn encode_audio_frame( 124 | mut frame: Option, 125 | output_format_context: &mut AVFormatContextOutput, 126 | encode_context: &mut AVCodecContext, 127 | ) -> Result<()> { 128 | static PTS: AtomicI64 = AtomicI64::new(0); 129 | 130 | if let Some(frame) = frame.as_mut() { 131 | frame.set_pts(PTS.fetch_add(frame.nb_samples as i64, Ordering::Relaxed)); 132 | } 133 | 134 | encode_context.send_frame(frame.as_ref())?; 135 | loop { 136 | let mut packet = match encode_context.receive_packet() { 137 | Ok(packet) => packet, 138 | Err(RsmpegError::EncoderDrainError) | Err(RsmpegError::EncoderFlushedError) => { 139 | break; 140 | } 141 | Err(e) => Err(e).context("Could not encode frame")?, 142 | }; 143 | 144 | output_format_context 145 | .write_frame(&mut packet) 146 | .context("Could not write frame")?; 147 | } 148 | Ok(()) 149 | } 150 | 151 | fn load_encode_and_write( 152 | fifo: &mut AVAudioFifo, 153 | output_format_context: &mut AVFormatContextOutput, 154 | encode_context: &mut AVCodecContext, 155 | ) -> Result<()> { 156 | let frame_size = fifo.size().min(encode_context.frame_size); 157 | let mut frame = init_output_frame( 158 | frame_size, 159 | encode_context.ch_layout().clone().into_inner(), 160 | encode_context.sample_fmt, 161 | encode_context.sample_rate, 162 | )?; 163 | if unsafe { fifo.read(frame.data_mut().as_mut_ptr(), frame_size)? } < frame_size { 164 | bail!("Could not read data from FIFO"); 165 | } 166 | encode_audio_frame(Some(frame), output_format_context, encode_context)?; 167 | Ok(()) 168 | } 169 | 170 | fn transcode_aac(input_file: &CStr, output_file: &CStr) -> Result<()> { 171 | // Open the input file for reading. 172 | let (mut input_format_context, mut decode_context, audio_stream_index) = 173 | open_input_file(input_file)?; 174 | 175 | // Open the output file for writing. 176 | let (mut output_format_context, mut encode_context) = 177 | open_output_file(output_file, &decode_context)?; 178 | 179 | // Initialize the resampler to be able to convert audio sample formats. 180 | let mut resample_context = init_resampler(&mut decode_context, &mut encode_context)?; 181 | 182 | // Initialize the FIFO buffer to store audio samples to be encoded. 183 | let mut fifo = AVAudioFifo::new( 184 | encode_context.sample_fmt, 185 | encode_context.ch_layout.nb_channels, 186 | 1, 187 | ); 188 | 189 | // Write the header of the output file container. 190 | output_format_context 191 | .write_header(&mut None) 192 | .context("Could not write output file header")?; 193 | 194 | // Loop as long as we have input samples to read or output samples to write; 195 | // abort as soon as we have neither. 196 | loop { 197 | let output_frame_size = encode_context.frame_size; 198 | 199 | loop { 200 | // We get enough audio samples. 201 | if fifo.size() >= output_frame_size { 202 | break; 203 | } 204 | 205 | // Break when no more input packets. 206 | let packet = match input_format_context 207 | .read_packet() 208 | .context("Could not read frame")? 209 | { 210 | Some(x) => x, 211 | None => break, 212 | }; 213 | 214 | // Ignore non audio stream packets. 215 | if packet.stream_index as usize != audio_stream_index { 216 | continue; 217 | } 218 | 219 | decode_context 220 | .send_packet(Some(&packet)) 221 | .context("Could not send packet for decoding")?; 222 | 223 | loop { 224 | let frame = match decode_context.receive_frame() { 225 | Ok(frame) => frame, 226 | Err(RsmpegError::DecoderDrainError) | Err(RsmpegError::DecoderFlushedError) => { 227 | break; 228 | } 229 | Err(e) => Err(e).context("Could not decode frame")?, 230 | }; 231 | 232 | let mut output_samples = AVSamples::new( 233 | encode_context.ch_layout.nb_channels, 234 | frame.nb_samples, 235 | encode_context.sample_fmt, 236 | 0, 237 | ) 238 | .context("Create samples buffer failed.")?; 239 | 240 | unsafe { 241 | resample_context 242 | .convert( 243 | output_samples.audio_data.as_mut_ptr(), 244 | output_samples.nb_samples, 245 | frame.extended_data as *const _, 246 | frame.nb_samples, 247 | ) 248 | .context("Could not convert input samples")?; 249 | } 250 | 251 | add_samples_to_fifo(&mut fifo, &output_samples, frame.nb_samples)?; 252 | } 253 | } 254 | 255 | // If we still cannot get enough samples, break. 256 | if fifo.size() < output_frame_size { 257 | break; 258 | } 259 | 260 | // Write frame as much as possible. 261 | while fifo.size() >= output_frame_size { 262 | load_encode_and_write(&mut fifo, &mut output_format_context, &mut encode_context)?; 263 | } 264 | } 265 | 266 | // Flush encode context 267 | encode_audio_frame(None, &mut output_format_context, &mut encode_context)?; 268 | 269 | output_format_context.write_trailer()?; 270 | 271 | Ok(()) 272 | } 273 | 274 | #[test] 275 | #[ignore = "transcode_aac_test0 测试运行依赖测试文件,暂时忽略"] 276 | fn transcode_aac_test0() { 277 | std::fs::create_dir_all("tests/output/transcode_aac/").unwrap(); 278 | transcode_aac( 279 | c"tests/assets/audios/sample1_short.aac", 280 | c"tests/output/transcode_aac/output_short.aac", 281 | ) 282 | .unwrap(); 283 | } 284 | 285 | #[test] 286 | #[ignore = "transcode_aac_test1 测试运行依赖测试文件,暂时忽略"] 287 | fn transcode_aac_test1() { 288 | std::fs::create_dir_all("tests/output/transcode_aac/").unwrap(); 289 | transcode_aac( 290 | c"tests/assets/vids/big_buck_bunny.mp4", 291 | c"tests/output/transcode_aac/big_buck_bunny.aac", 292 | ) 293 | .unwrap(); 294 | } 295 | -------------------------------------------------------------------------------- /tests/vaapi_encode.rs: -------------------------------------------------------------------------------- 1 | //! RIIR: https://github.com/FFmpeg/FFmpeg/blob/master/doc/examples/vaapi_encode.c 2 | use anyhow::{Context, Result}; 3 | use rsmpeg::{ 4 | avcodec::{AVCodec, AVCodecContext}, 5 | avutil::{ra, AVFrame, AVHWDeviceContext}, 6 | error::RsmpegError, 7 | ffi::{ 8 | AVHWDeviceType, AVPixelFormat, AV_HWDEVICE_TYPE_CUDA, AV_HWDEVICE_TYPE_VAAPI, 9 | AV_HWDEVICE_TYPE_VIDEOTOOLBOX, AV_PIX_FMT_CUDA, AV_PIX_FMT_NV12, AV_PIX_FMT_VAAPI, 10 | AV_PIX_FMT_VIDEOTOOLBOX, 11 | }, 12 | }; 13 | use std::{ 14 | ffi::CStr, 15 | fs::File, 16 | io::{self, Read, Write}, 17 | path::Path, 18 | slice, 19 | }; 20 | 21 | fn set_hwframe_ctx( 22 | avctx: &mut AVCodecContext, 23 | hw_device_ctx: &AVHWDeviceContext, 24 | width: i32, 25 | height: i32, 26 | hw_format: AVPixelFormat, 27 | sw_format: AVPixelFormat, 28 | ) -> Result<()> { 29 | let mut hw_frames_ref = hw_device_ctx.hwframe_ctx_alloc(); 30 | hw_frames_ref.data().format = hw_format; 31 | hw_frames_ref.data().sw_format = sw_format; 32 | hw_frames_ref.data().width = width; 33 | hw_frames_ref.data().height = height; 34 | hw_frames_ref.data().initial_pool_size = 20; 35 | 36 | hw_frames_ref 37 | .init() 38 | .context("Failed to initialize VAAPI frame context")?; 39 | 40 | avctx.set_hw_frames_ctx(hw_frames_ref); 41 | 42 | Ok(()) 43 | } 44 | 45 | fn encode_write( 46 | avctx: &mut AVCodecContext, 47 | frame: Option<&AVFrame>, 48 | fout: &mut File, 49 | ) -> Result<()> { 50 | avctx.send_frame(frame).context("Send frame failed")?; 51 | loop { 52 | let mut packet = match avctx.receive_packet() { 53 | Ok(packet) => packet, 54 | Err(RsmpegError::EncoderDrainError) | Err(RsmpegError::EncoderFlushedError) => { 55 | break; 56 | } 57 | Err(e) => Err(e).context("Receive packet failed.")?, 58 | }; 59 | packet.set_stream_index(0); 60 | let data = unsafe { slice::from_raw_parts(packet.data, packet.size as usize) }; 61 | fout.write_all(data).context("Write output frame failed.")?; 62 | } 63 | Ok(()) 64 | } 65 | 66 | fn hw_encode( 67 | input: &Path, 68 | output: &Path, 69 | width: i32, 70 | height: i32, 71 | encode_codec: &CStr, 72 | device_type: AVHWDeviceType, 73 | hw_format: AVPixelFormat, 74 | sw_format: AVPixelFormat, 75 | ) -> Result<()> { 76 | let size = width as usize * height as usize; 77 | 78 | let mut fin = File::open(input).context("Fail to open input file")?; 79 | let mut fout = File::create(output).context("Fail to open output file")?; 80 | 81 | let hw_device_ctx = AVHWDeviceContext::create(device_type, None, None, 0) 82 | .context("Failed to create a VAAPI device")?; 83 | 84 | let codec = AVCodec::find_encoder_by_name(encode_codec).context("Could not find encoder.")?; 85 | 86 | let mut avctx = AVCodecContext::new(&codec); 87 | 88 | avctx.set_width(width); 89 | avctx.set_height(height); 90 | avctx.set_time_base(ra(1, 25)); 91 | avctx.set_framerate(ra(25, 1)); 92 | avctx.set_sample_aspect_ratio(ra(1, 1)); 93 | avctx.set_pix_fmt(hw_format); 94 | 95 | set_hwframe_ctx( 96 | &mut avctx, 97 | &hw_device_ctx, 98 | width, 99 | height, 100 | hw_format, 101 | sw_format, 102 | ) 103 | .context("Failed to set hwframe context.")?; 104 | 105 | avctx 106 | .open(None) 107 | .context("Cannot open video encoder codec")?; 108 | 109 | loop { 110 | let mut sw_frame = AVFrame::new(); 111 | 112 | // read data into software frame, and transfer them into hw frame 113 | sw_frame.set_width(width); 114 | sw_frame.set_height(height); 115 | sw_frame.set_format(sw_format); 116 | sw_frame.get_buffer(0).context("Get buffer failed.")?; 117 | 118 | let y = unsafe { slice::from_raw_parts_mut(sw_frame.data_mut()[0], size) }; 119 | match fin.read_exact(y) { 120 | Ok(()) => {} 121 | Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => break, 122 | e @ Err(_) => e.context("Read Y failed.")?, 123 | } 124 | let uv = unsafe { slice::from_raw_parts_mut(sw_frame.data_mut()[1], size / 2) }; 125 | match fin.read_exact(uv) { 126 | Ok(()) => {} 127 | Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => break, 128 | e @ Err(_) => e.context("Read UV failed.")?, 129 | } 130 | 131 | let mut hw_frame = AVFrame::new(); 132 | avctx 133 | .hw_frames_ctx_mut() 134 | .unwrap() 135 | .get_buffer(&mut hw_frame) 136 | .context("Get buffer failed")?; 137 | hw_frame 138 | .hwframe_transfer_data(&sw_frame) 139 | .context("Error while transferring frame data to surface.")?; 140 | 141 | encode_write(&mut avctx, Some(&hw_frame), &mut fout).context("Failed to encode.")?; 142 | } 143 | 144 | encode_write(&mut avctx, None, &mut fout).context("Failed to encode.")?; 145 | Ok(()) 146 | } 147 | 148 | #[test] 149 | #[ignore = "Github actions doesn't have vaapi device"] 150 | fn vaapi_encode_test_vaapi() { 151 | std::fs::create_dir_all("tests/output/vaapi_encode/").unwrap(); 152 | // Produced by ffmpeg -i tests/assets/vids/bear.mp4 -pix_fmt nv12 tests/assets/vids/bear.yuv 153 | hw_encode( 154 | Path::new("tests/assets/vids/bear.yuv"), 155 | Path::new("tests/output/vaapi_encode/vaapi_encode_test_vaapi.h264"), 156 | 320, 157 | 180, 158 | c"h264_vaapi", 159 | AV_HWDEVICE_TYPE_VAAPI, 160 | AV_PIX_FMT_VAAPI, 161 | AV_PIX_FMT_NV12, 162 | ) 163 | .unwrap(); 164 | } 165 | 166 | /// You should test this with nvenc enabled in compilation(e.g. utils/linux_ffmpeg.rs) https://trac.ffmpeg.org/wiki/HWAccelIntro#NVENC 167 | /// 168 | /// I use this rather than vaapi test(since I don't have a vaapi compatible device). 169 | /// 170 | /// They are almost the same, the only differences: 171 | /// 172 | /// - device_type: AV_HWDEVICE_TYPE_CUDA for nvenc, AV_HWDEVICE_TYPE_VAAPI for vaapi 173 | /// - hw_format: AV_PIX_FMT_CUDA for nvenc, AV_PIX_FMT_VAAPI for vaapi 174 | #[test] 175 | #[ignore = "Github actions doesn't have nvdia graphics card"] 176 | fn nvenc_encode_test_nvenc() { 177 | std::fs::create_dir_all("tests/output/nvenc_encode/").unwrap(); 178 | // Produced by ffmpeg -i tests/assets/vids/bear.mp4 -pix_fmt nv12 tests/assets/vids/bear.yuv 179 | hw_encode( 180 | Path::new("tests/assets/vids/bear.yuv"), 181 | Path::new("tests/output/nvenc_encode/nvenc_encode_test_nvenc.h264"), 182 | 320, 183 | 180, 184 | c"h264_nvenc", 185 | AV_HWDEVICE_TYPE_CUDA, 186 | AV_PIX_FMT_CUDA, 187 | AV_PIX_FMT_NV12, 188 | ) 189 | .unwrap(); 190 | } 191 | 192 | #[test] 193 | #[ignore = "Github actions doesn't have macOS videotoolbox graphics card"] 194 | fn toolbox_encode_test_videotoolbox() { 195 | std::fs::create_dir_all("tests/output/toolbox_encode/").unwrap(); 196 | // Produced by ffmpeg -i tests/assets/vids/bear.mp4 -pix_fmt nv12 tests/assets/vids/bear.yuv 197 | hw_encode( 198 | Path::new("tests/assets/vids/bear.yuv"), 199 | Path::new("tests/output/toolbox_encode/toolbox_encode_test_h264.h264"), 200 | 320, 201 | 180, 202 | c"h264_videotoolbox", 203 | AV_HWDEVICE_TYPE_VIDEOTOOLBOX, 204 | AV_PIX_FMT_VIDEOTOOLBOX, 205 | AV_PIX_FMT_NV12, 206 | ) 207 | .unwrap(); 208 | } 209 | -------------------------------------------------------------------------------- /tests/vaapi_transcode.rs: -------------------------------------------------------------------------------- 1 | //! RIIR: https://github.com/FFmpeg/FFmpeg/blob/master/doc/examples/vaapi_transcode.c 2 | 3 | use anyhow::{anyhow, bail, Context, Error, Result}; 4 | use std::ffi::CStr; 5 | 6 | use rsmpeg::avcodec::{AVCodec, AVCodecContext}; 7 | use rsmpeg::avformat::{AVFormatContextInput, AVFormatContextOutput}; 8 | use rsmpeg::avutil::{self, AVFrame, AVHWDeviceContext, AVPixelFormat}; 9 | use rsmpeg::error::RsmpegError; 10 | use rsmpeg::ffi; 11 | use rsmpeg::ffi::{ 12 | AVHWDeviceType, AV_HWDEVICE_TYPE_CUDA, AV_HWDEVICE_TYPE_VAAPI, AV_PIX_FMT_CUDA, 13 | AV_PIX_FMT_NV12, AV_PIX_FMT_VAAPI, 14 | }; 15 | 16 | #[no_mangle] 17 | unsafe extern "C" fn hwaccel_get_format( 18 | ctx: *mut ffi::AVCodecContext, 19 | pix_fmts: *const ffi::AVPixelFormat, 20 | ) -> ffi::AVPixelFormat { 21 | let mut p = pix_fmts; 22 | let hw_format = (*ctx).opaque as ffi::AVPixelFormat; 23 | while *p != ffi::AV_PIX_FMT_NONE { 24 | if *p == hw_format { 25 | return *p; 26 | } 27 | p = p.add(1); 28 | } 29 | ffi::AV_PIX_FMT_NONE 30 | } 31 | 32 | fn set_hwframe_ctx( 33 | is_decoder: bool, 34 | codec_ctx: &mut AVCodecContext, 35 | hw_device_ctx: &AVHWDeviceContext, 36 | width: i32, 37 | height: i32, 38 | hw_format: AVPixelFormat, 39 | sw_format: AVPixelFormat, 40 | ) -> Result<()> { 41 | let mut hw_frames_ref = hw_device_ctx.hwframe_ctx_alloc(); 42 | hw_frames_ref.data().format = hw_format; 43 | hw_frames_ref.data().sw_format = sw_format; 44 | hw_frames_ref.data().width = width; 45 | hw_frames_ref.data().height = height; 46 | hw_frames_ref.data().initial_pool_size = 20; 47 | 48 | hw_frames_ref 49 | .init() 50 | .context("Failed to initialize VAAPI frame context")?; 51 | 52 | codec_ctx.set_hw_frames_ctx(hw_frames_ref); 53 | codec_ctx.set_pix_fmt(hw_format); 54 | 55 | if is_decoder { 56 | unsafe { 57 | let hw_device_ctx_ptr = hw_device_ctx.as_ptr(); 58 | let codec_ctx_ptr = codec_ctx.as_mut_ptr(); 59 | (*codec_ctx_ptr).opaque = hw_format as *mut std::os::raw::c_void; 60 | (*codec_ctx_ptr).get_format = Some(hwaccel_get_format); 61 | (*codec_ctx_ptr).sw_pix_fmt = sw_format; 62 | (*codec_ctx_ptr).hw_device_ctx = hw_device_ctx_ptr as *mut _; 63 | } 64 | } 65 | 66 | Ok(()) 67 | } 68 | 69 | fn open_input_file( 70 | filename: &CStr, 71 | decode_codec_name: &CStr, 72 | hw_device_ctx: &AVHWDeviceContext, 73 | hw_format: AVPixelFormat, 74 | sw_format: AVPixelFormat, 75 | ) -> Result<(AVCodecContext, AVFormatContextInput, usize)> { 76 | let mut ifmt_ctx = AVFormatContextInput::open(filename, None, &mut None)?; 77 | let (video_index, _decode_codec) = ifmt_ctx 78 | .find_best_stream(ffi::AVMEDIA_TYPE_VIDEO)? 79 | .context("Failed to find video stream")?; 80 | let video_stream = &ifmt_ctx.streams()[video_index]; 81 | 82 | let decode_codec = AVCodec::find_decoder_by_name(decode_codec_name).with_context(|| { 83 | anyhow!( 84 | "Failed to find decoder codec: {}", 85 | decode_codec_name.to_str().unwrap() 86 | ) 87 | })?; 88 | let mut decode_ctx = AVCodecContext::new(&decode_codec); 89 | let time_base = avutil::ra(1, 24); 90 | decode_ctx.set_time_base(time_base); 91 | decode_ctx.set_sample_aspect_ratio(avutil::ra(1, 1)); 92 | decode_ctx.apply_codecpar(&video_stream.codecpar())?; 93 | 94 | let (width, height) = (decode_ctx.width, decode_ctx.height); 95 | set_hwframe_ctx( 96 | true, 97 | &mut decode_ctx, 98 | hw_device_ctx, 99 | width, 100 | height, 101 | hw_format, 102 | sw_format, 103 | )?; 104 | 105 | decode_ctx 106 | .open(None) 107 | .with_context(|| anyhow!("Failed to open decoder context"))?; 108 | 109 | ifmt_ctx.dump(0, filename)?; 110 | 111 | Ok((decode_ctx, ifmt_ctx, video_index)) 112 | } 113 | 114 | fn open_output_file( 115 | filename: &CStr, 116 | encode_codec_name: &CStr, 117 | decode_ctx: &AVCodecContext, 118 | hw_device_ctx: &AVHWDeviceContext, 119 | hw_format: AVPixelFormat, 120 | sw_format: AVPixelFormat, 121 | ) -> Result<(AVCodecContext, AVFormatContextOutput, usize)> { 122 | let mut ofmt_ctx = AVFormatContextOutput::create(filename, None)?; 123 | 124 | let encode_codec = AVCodec::find_encoder_by_name(encode_codec_name).with_context(|| { 125 | anyhow!( 126 | "Failed to find encoder codec: {}", 127 | encode_codec_name.to_str().unwrap() 128 | ) 129 | })?; 130 | let mut encode_ctx = AVCodecContext::new(&encode_codec); 131 | encode_ctx.set_width(decode_ctx.width); 132 | encode_ctx.set_height(decode_ctx.height); 133 | encode_ctx.set_framerate(decode_ctx.framerate); 134 | encode_ctx.set_time_base(decode_ctx.time_base); 135 | encode_ctx.set_sample_aspect_ratio(decode_ctx.sample_aspect_ratio); 136 | 137 | // Some formats want stream headers to be separate. 138 | if ofmt_ctx.oformat().flags & ffi::AVFMT_GLOBALHEADER as i32 != 0 { 139 | encode_ctx.set_flags(encode_ctx.flags | ffi::AV_CODEC_FLAG_GLOBAL_HEADER as i32); 140 | } 141 | 142 | let (width, height) = (encode_ctx.width, encode_ctx.height); 143 | 144 | set_hwframe_ctx( 145 | false, 146 | &mut encode_ctx, 147 | hw_device_ctx, 148 | width, 149 | height, 150 | hw_format, 151 | sw_format, 152 | )?; 153 | 154 | encode_ctx.open(None).with_context(|| { 155 | anyhow!( 156 | "Cannot open {} encoder context.", 157 | encode_codec.name().to_str().unwrap() 158 | ) 159 | })?; 160 | 161 | let out_stream_index = { 162 | let mut out_stream = ofmt_ctx.new_stream(); 163 | out_stream.set_codecpar(encode_ctx.extract_codecpar()); 164 | out_stream.set_time_base(encode_ctx.time_base); 165 | out_stream.index 166 | }; 167 | 168 | Ok((encode_ctx, ofmt_ctx, out_stream_index as usize)) 169 | } 170 | 171 | /// hw_frame -> sw_frame 172 | fn hw_download(hw_frame: AVFrame, sw_format: AVPixelFormat) -> Result { 173 | let mut sw_frame = AVFrame::new(); 174 | sw_frame.set_width(hw_frame.width); 175 | sw_frame.set_height(hw_frame.height); 176 | sw_frame.set_format(sw_format); 177 | sw_frame 178 | .alloc_buffer() 179 | .context("Failed to allocate software frame buffer")?; 180 | 181 | sw_frame 182 | .hwframe_transfer_data(&hw_frame) 183 | .context("Failed to transfer data from hardware frame to software frame")?; 184 | 185 | sw_frame.set_pts(hw_frame.pts); 186 | sw_frame.set_time_base(hw_frame.time_base); 187 | sw_frame.set_sample_rate(hw_frame.sample_rate); 188 | 189 | Ok(sw_frame) 190 | } 191 | 192 | /// sw_frame -> hw_frame 193 | fn hw_upload( 194 | encode_ctx: &mut AVCodecContext, 195 | sw_frame: AVFrame, 196 | hw_format: AVPixelFormat, 197 | ) -> Result { 198 | let mut hw_frames_ctx = encode_ctx 199 | .hw_frames_ctx_mut() 200 | .ok_or_else(|| Error::msg("Encoder has no hardware frames context"))?; 201 | 202 | let mut hw_frame = AVFrame::new(); 203 | hw_frame.set_width(sw_frame.width); 204 | hw_frame.set_height(sw_frame.height); 205 | hw_frame.set_format(hw_format); 206 | unsafe { 207 | (*hw_frame.as_mut_ptr()).hw_frames_ctx = hw_frames_ctx.as_mut_ptr(); 208 | } 209 | 210 | hw_frames_ctx 211 | .get_buffer(&mut hw_frame) 212 | .context("Failed to allocate hardware frame buffer")?; 213 | 214 | hw_frame 215 | .hwframe_transfer_data(&sw_frame) 216 | .context("Failed to transfer data from software frame to hardware frame")?; 217 | 218 | hw_frame.set_pts(sw_frame.pts); 219 | hw_frame.set_time_base(sw_frame.time_base); 220 | hw_frame.set_sample_rate(sw_frame.sample_rate); 221 | 222 | Ok(hw_frame) 223 | } 224 | 225 | /// Send an empty packet to the `encode_context` for packet flushing. 226 | fn flush_encoder( 227 | enc_ctx: &mut AVCodecContext, 228 | ofmt_ctx: &mut AVFormatContextOutput, 229 | stream_index: usize, 230 | ) -> Result<()> { 231 | if enc_ctx.codec().capabilities & ffi::AV_CODEC_CAP_DELAY as i32 == 0 { 232 | return Ok(()); 233 | } 234 | encode_write_frame(None, enc_ctx, ofmt_ctx, stream_index)?; 235 | Ok(()) 236 | } 237 | 238 | fn encode_write_frame( 239 | mut filt_frame: Option, 240 | enc_ctx: &mut AVCodecContext, 241 | ofmt_ctx: &mut AVFormatContextOutput, 242 | stream_index: usize, 243 | ) -> Result<()> { 244 | if let Some(filt_frame) = filt_frame.as_mut() { 245 | if filt_frame.pts != ffi::AV_NOPTS_VALUE { 246 | filt_frame.set_pts(avutil::av_rescale_q( 247 | filt_frame.pts, 248 | filt_frame.time_base, 249 | enc_ctx.time_base, 250 | )); 251 | } 252 | } 253 | 254 | enc_ctx 255 | .send_frame(filt_frame.as_ref()) 256 | .context("Encode frame failed.")?; 257 | 258 | loop { 259 | let mut enc_pkt = match enc_ctx.receive_packet() { 260 | Ok(packet) => packet, 261 | Err(RsmpegError::EncoderDrainError) | Err(RsmpegError::EncoderFlushedError) => break, 262 | Err(e) => bail!(e), 263 | }; 264 | 265 | enc_pkt.set_pos(-1); 266 | enc_pkt.set_stream_index(stream_index as i32); 267 | enc_pkt.rescale_ts( 268 | enc_ctx.time_base, 269 | ofmt_ctx.streams()[stream_index].time_base, 270 | ); 271 | 272 | ofmt_ctx 273 | .interleaved_write_frame(&mut enc_pkt) 274 | .context("Interleaved write frame failed.")?; 275 | } 276 | 277 | Ok(()) 278 | } 279 | 280 | fn hw_transcode( 281 | input: &CStr, 282 | output: &CStr, 283 | decode_codec: &CStr, 284 | encode_codec: &CStr, 285 | device_type: AVHWDeviceType, 286 | hw_format: AVPixelFormat, 287 | sw_format: AVPixelFormat, 288 | ) -> Result<()> { 289 | let hw_device_ctx = AVHWDeviceContext::create(device_type, None, None, 0) 290 | .context("Failed to create a hw device context")?; 291 | 292 | let (mut decode_ctx, mut ifmt_ctx, input_stream_index) = 293 | open_input_file(input, decode_codec, &hw_device_ctx, hw_format, sw_format)?; 294 | 295 | let (mut encode_ctx, mut ofmt_ctx, out_stream_index) = open_output_file( 296 | output, 297 | encode_codec, 298 | &decode_ctx, 299 | &hw_device_ctx, 300 | hw_format, 301 | sw_format, 302 | )?; 303 | 304 | ofmt_ctx 305 | .write_header(&mut None) 306 | .context("Failed to write output header")?; 307 | 308 | loop { 309 | let packet = match ifmt_ctx.read_packet() { 310 | Ok(Some(x)) => x, 311 | // No more frames 312 | Ok(None) => break, 313 | Err(e) => bail!("Read frame error: {:?}", e), 314 | }; 315 | 316 | // Skip if not video packet 317 | if packet.stream_index != input_stream_index as i32 { 318 | continue; 319 | } 320 | 321 | decode_ctx 322 | .send_packet(Some(&packet)) 323 | .context("Send packet error.")?; 324 | 325 | loop { 326 | let hw_frame = match decode_ctx.receive_frame() { 327 | Ok(frame) => frame, 328 | Err(RsmpegError::DecoderDrainError) | Err(RsmpegError::DecoderFlushedError) => { 329 | break; 330 | } 331 | Err(e) => bail!(e), 332 | }; 333 | 334 | assert_eq!(hw_frame.format, hw_format); 335 | assert!( 336 | !hw_frame.hw_frames_ctx.is_null(), 337 | "HW frame context is null" 338 | ); 339 | 340 | let download_sw_frame = hw_download(hw_frame, sw_format)?; 341 | 342 | // do something process, scaler frame etc. 343 | log::info!("{:?}", download_sw_frame); 344 | 345 | let upload_hw_frame = hw_upload(&mut encode_ctx, download_sw_frame, hw_format)?; 346 | 347 | encode_write_frame( 348 | Some(upload_hw_frame), 349 | &mut encode_ctx, 350 | &mut ofmt_ctx, 351 | out_stream_index, 352 | )? 353 | } 354 | } 355 | 356 | flush_encoder(&mut encode_ctx, &mut ofmt_ctx, out_stream_index)?; 357 | ofmt_ctx.write_trailer()?; 358 | 359 | Ok(()) 360 | } 361 | 362 | #[test] 363 | #[ignore = "Github actions doesn't have vaapi device"] 364 | fn vaapi_transcode_test_vaapi() { 365 | std::fs::create_dir_all("tests/output/vaapi_transcode/").unwrap(); 366 | 367 | hw_transcode( 368 | c"tests/assets/vids/bear.mp4", 369 | c"tests/output/vaapi_transcode/vaapi_transcode_h264_vaapi.mp4", 370 | c"h264_vaapi", 371 | c"h264_vaapi", 372 | AV_HWDEVICE_TYPE_VAAPI, 373 | AV_PIX_FMT_VAAPI, 374 | AV_PIX_FMT_NV12, 375 | ) 376 | .unwrap(); 377 | } 378 | 379 | #[test] 380 | #[ignore = "Github actions doesn't have nvdia graphics card"] 381 | fn nvenc_transcode_test_nvenc() { 382 | std::fs::create_dir_all("tests/output/nvenc_transcode/").unwrap(); 383 | hw_transcode( 384 | c"tests/assets/vids/bear.mp4", 385 | c"tests/output/nvenc_transcode/nvenc_transcode_h264_nvenc.mp4", 386 | c"h264_cuvid", 387 | c"h264_nvenc", 388 | AV_HWDEVICE_TYPE_CUDA, 389 | AV_PIX_FMT_CUDA, 390 | AV_PIX_FMT_NV12, 391 | ) 392 | .unwrap(); 393 | } 394 | --------------------------------------------------------------------------------