diff --git a/.github/workflows/rust-release.yml b/.github/workflows/rust-release.yml index c6ac8d0..452b578 100644 --- a/.github/workflows/rust-release.yml +++ b/.github/workflows/rust-release.yml @@ -6,39 +6,39 @@ on: - v* jobs: - publish: - name: Publish for ${{ matrix.os }} + build: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-18.04,windows-latest,macOS-latest] + os: [ubuntu-18.04, macOS-latest] include: - name: linux os: ubuntu-18.04 artifact_name: target/release/RustPlayer - asset_name: RustPlayer-linux - - name: windows - os: windows-latest - artifact_name: target/release/RustPlayer.exe - asset_name: RustPlayer-windows.exe + asset_name: RustPlayer-linux-amd64 - name: macos os: macos-latest artifact_name: target/release/RustPlayer asset_name: RustPlayer-macos - + # - name: windows + # os: windows-latest + # artifact_name: target/release/RustPlayer.exe + # asset_name: RustPlayer-windows.exe steps: - - uses: actions/checkout@v1 - + - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 with: - profile: minimal toolchain: stable - - - name: Install libasound2-dev for Linux + + - name: install deps for linux if: matrix.os == 'ubuntu-18.04' - run: sudo apt install -y libasound2-dev + run: sudo apt update && sudo apt install -y libasound2-dev libavcodec-dev libavformat-dev libswresample-dev libavutil-dev libavformat-dev pkg-config + + - name: install ffmpeg deps for macOS + if: matrix.os == 'macos-latest' + run: brew install ffmpeg@4 pkg-config && brew link ffmpeg@4 - - name: Build + - name: build RustPlayer run: cargo build --release - name: Test @@ -49,3 +49,46 @@ jobs: with: name: ${{ matrix.asset_name }} path: ${{ matrix.artifact_name }} + + build-on-windows: + runs-on: windows-latest + strategy: + matrix: + include: + - { sys: mingw64, env: x86_64, artifact_name: target/release/RustPlayer.exe , asset_name: RustPlayer-windows-x86_64.exe } + # - { sys: mingw32, env: i686 } + # - { sys: ucrt64, env: ucrt-x86_64 } # Experimental! + # - { sys: clang64, env: clang-x86_64 } # Experimental! + # defaults: + # run: + # shell: msys2 {0} + steps: + - uses: actions/checkout@v2 + - name: Install msys2 for windows + uses: msys2/setup-msys2@v2 + with: + msystem: ${{matrix.sys}} + # update: true + install: >- + mingw-w64-${{matrix.env}}-ffmpeg + mingw-w64-${{matrix.env}}-llvm + mingw-w64-${{matrix.env}}-clang + mingw-w64-${{matrix.env}}-pkg-config + mingw-w64-${{matrix.env}}-rust + + - name: build RustPlayer + run: | + set MSYSTEM=MINGW64 + msys2 -c 'cargo build --release' + + - name: Test + run: | + set MSYSTEM=MINGW64 + msys2 -c 'cargo test' + + - name: Upload binaries to release + uses: actions/upload-artifact@v2 + with: + name: ${{ matrix.asset_name }} + path: ${{ matrix.artifact_name }} + diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index d8f79bb..e958941 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -18,11 +18,78 @@ env: jobs: build: - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-18.04, macOS-latest] + include: + - name: linux + os: ubuntu-18.04 + artifact_name: target/release/RustPlayer + - name: macos + os: macos-latest + artifact_name: target/release/RustPlayer + # - name: windows + # os: windows-latest + # artifact_name: target/release/RustPlayer.exe + # asset_name: RustPlayer-windows.exe steps: - uses: actions/checkout@v2 - uses: actions-rs/toolchain@v1 with: toolchain: stable - - run: sudo apt install -y libasound2-dev - - run: cargo build --release + + - name: install deps for linux + if: matrix.os == 'ubuntu-18.04' + run: sudo apt update && sudo apt install -y libasound2-dev libavcodec-dev libavformat-dev libswresample-dev libavutil-dev libavformat-dev pkg-config + + - name: install ffmpeg deps for macOS + if: matrix.os == 'macos-latest' + run: brew install ffmpeg@4 pkg-config && brew link ffmpeg@4 + + - name: build RustPlayer + run: cargo build --release + + - name: Test + run: cargo test + + # - name: Upload binaries to release + # uses: actions/upload-artifact@v2 + # with: + # name: ${{ matrix.asset_name }} + # path: ${{ matrix.artifact_name }} + build-on-windows: + runs-on: windows-latest + strategy: + matrix: + include: + - { sys: mingw64, env: x86_64, artifact_name: target/release/RustPlayer } + # - { sys: mingw32, env: i686 } + # - { sys: ucrt64, env: ucrt-x86_64 } # Experimental! + # - { sys: clang64, env: clang-x86_64 } # Experimental! + # defaults: + # run: + # shell: msys2 {0} + steps: + - uses: actions/checkout@v2 + - name: Install msys2 for windows + uses: msys2/setup-msys2@v2 + with: + msystem: ${{matrix.sys}} + # update: true + install: >- + mingw-w64-${{matrix.env}}-ffmpeg + mingw-w64-${{matrix.env}}-llvm + mingw-w64-${{matrix.env}}-clang + mingw-w64-${{matrix.env}}-pkg-config + mingw-w64-${{matrix.env}}-rust + + - name: build RustPlayer + run: | + set MSYSTEM=MINGW64 + msys2 -c 'cargo build --release' + + - name: Test + run: | + set MSYSTEM=MINGW64 + msys2 -c 'cargo test' diff --git a/.idea/vcs.xml b/.idea/vcs.xml index 35eb1dd..bc932cf 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -2,5 +2,6 @@ + \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 917a7ba..f382b37 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,12 +4,13 @@ version = 3 [[package]] name = "RustPlayer" -version = "0.1.0" +version = "1.1.0" dependencies = [ "bytes", "crossterm 0.23.0", "dirs", "failure", + "ffmpeg-decoder", "m3u8-rs", "mp3-duration", "open", @@ -81,7 +82,7 @@ checksum = "5e121dee8023ce33ab248d9ce1493df03c3b38a659b240096fcbd7048ff9c31f" dependencies = [ "addr2line", "cc", - "cfg-if", + "cfg-if 1.0.0", "libc", "miniz_oxide", "object", @@ -96,12 +97,33 @@ checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" [[package]] name = "bindgen" -version = "0.56.0" +version = "0.53.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c72a978d268b1d70b0e963217e60fdabd9523a941457a6c42a7315d15c7e89e5" +dependencies = [ + "bitflags", + "cexpr", + "cfg-if 0.1.10", + "clang-sys", + "lazy_static", + "lazycell", + "peeking_take_while", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", +] + +[[package]] +name = "bindgen" +version = "0.54.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2da379dbebc0b76ef63ca68d8fc6e71c0f13e59432e0987e508c1820e6ab5239" +checksum = "66c0bb6167449588ff70803f4127f0684f9063097eca5016f37eb52b92c2cf36" dependencies = [ "bitflags", "cexpr", + "cfg-if 0.1.10", "clang-sys", "lazy_static", "lazycell", @@ -167,6 +189,12 @@ dependencies = [ "nom 5.1.2", ] +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + [[package]] name = "cfg-if" version = "1.0.0" @@ -175,9 +203,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clang-sys" -version = "1.3.1" +version = "0.29.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cc00842eed744b858222c4c9faf7243aafc6d33f92f96935263ef4d8a41ce21" +checksum = "fe6837df1d5cba2397b835c8530f51723267e16abbf83892e9e5af4f0e5dd10a" dependencies = [ "glob", "libc", @@ -228,11 +256,11 @@ dependencies = [ [[package]] name = "coreaudio-sys" -version = "0.2.9" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca4679a59dbd8c15f064c012dfe8c1163b9453224238b59bb9328c142b8b248b" +checksum = "17f73df0f29f4c3c374854f076c47dc018f19acaa63538880dba0937ad4fa8d7" dependencies = [ - "bindgen", + "bindgen 0.53.3", ] [[package]] @@ -362,7 +390,7 @@ version = "0.8.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7896dc8abb250ffdda33912550faa54c88ec8b998dec0b2c55ab224921ce11df" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", ] [[package]] @@ -396,6 +424,30 @@ dependencies = [ "instant", ] +[[package]] +name = "ffmpeg-decoder" +version = "0.1.3" +dependencies = [ + "ffmpeg-sys-next", + "log", + "rodio", + "thiserror", +] + +[[package]] +name = "ffmpeg-sys-next" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de57234f2c49c6e093fe67bbbaa9142c228f6e2d5533ef27980993d5b6adef2a" +dependencies = [ + "bindgen 0.54.0", + "cc", + "libc", + "num_cpus", + "pkg-config", + "vcpkg", +] + [[package]] name = "fnv" version = "1.0.7" @@ -472,7 +524,7 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d39cd93900197114fa1fcb7ae84ca742095eed9442088988ae74fa744e930e77" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "libc", "wasi 0.10.2+wasi-snapshot-preview1", ] @@ -633,7 +685,7 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", ] [[package]] @@ -717,11 +769,11 @@ checksum = "1bf2e165bb3457c8e098ea76f3e3bc9db55f87aa90d52d0e6be741470916aaa4" [[package]] name = "libloading" -version = "0.7.3" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efbc0f03f9a775e9f6aed295c6a1ba2253c5757a9e03d55c6caa46a681abcddd" +checksum = "f2b111a074963af1d37a139918ac6d49ad1d0d5e47f72fd55388619691a7d753" dependencies = [ - "cfg-if", + "cc", "winapi", ] @@ -740,7 +792,7 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", ] [[package]] @@ -951,7 +1003,7 @@ checksum = "9f866317acbd3a240710c63f065ffb1e4fd466259045ccb504130b7f668f35c6" dependencies = [ "bitflags", "cc", - "cfg-if", + "cfg-if 1.0.0", "libc", "memoffset", ] @@ -1017,18 +1069,18 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.5.6" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "720d3ea1055e4e4574c0c0b0f8c3fd4f24c4cdaf465948206dea090b57b526ad" +checksum = "cf5395665662ef45796a4ff5486c5d41d29e0c09640af4c5f17fd94ee2c119c9" dependencies = [ "num_enum_derive", ] [[package]] name = "num_enum_derive" -version = "0.5.6" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d992b768490d7fe0d8586d9b5745f6c49f557da6d81dc982b1d167ad4edbb21" +checksum = "3b0498641e53dd6ac1a4f22547548caa6864cc4933784319cd1775271c5a46ce" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -1100,7 +1152,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c7ae222234c30df141154f159066c5093ff73b63204dcda7121eb082fc56a95" dependencies = [ "bitflags", - "cfg-if", + "cfg-if 1.0.0", "foreign-types", "libc", "once_cell", @@ -1153,7 +1205,7 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "instant", "libc", "redox_syscall", @@ -1167,7 +1219,7 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28141e0cc4143da2443301914478dc976a61ffdb3f043058310c70df2fed8954" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "libc", "redox_syscall", "smallvec", @@ -1218,9 +1270,9 @@ checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" [[package]] name = "proc-macro-crate" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dada8c9981fcf32929c3c0f0cd796a9284aca335565227ed88c83babb1d43dc" +checksum = "e17d47ce914bf4de440332250b0edd23ce48c005f59fab39d3335866b114f11a" dependencies = [ "thiserror", "toml", @@ -1574,7 +1626,7 @@ version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "fastrand", "libc", "redox_syscall", @@ -1693,7 +1745,7 @@ version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a1bdf54a7c28a2bbf701e1d2233f6c77f473486b94bee4f9678da5a148dca7f" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "pin-project-lite", "tracing-core", ] @@ -1822,7 +1874,7 @@ version = "0.2.79" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25f1af7423d8588a3d840681122e72e6a24ddbcb3f0ec385cac0d12d24256c06" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "wasm-bindgen-macro", ] @@ -1847,7 +1899,7 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2eb6ec270a31b1d3c7e266b999739109abce8b6c87e4b31fcfcd788b65267395" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "js-sys", "wasm-bindgen", "web-sys", diff --git a/Cargo.toml b/Cargo.toml index 79a3b04..992de28 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "RustPlayer" -version = "0.1.0" +version = "1.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -27,4 +27,4 @@ m3u8-rs = "3.0.0" dirs = "4.0.0" bytes = "1.1.0" -# ffmpeg-next = "5.0.3" \ No newline at end of file +ffmpeg-decoder = {features = ["rodio_source"], path = "thirdparty/ffmpeg-decoder-rs"} \ No newline at end of file diff --git a/README.md b/README.md index f362cfd..3ea6536 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,35 @@ -# RustPlayer [![GitHub tag](https://img.shields.io/github/tag/Kingtous/RustPlayer)](https://GitHub.com/Kingtous/RustPlayer/tags/) +# RustPlayer [![GitHub tag](https://img.shields.io/github/tag/Kingtous/RustPlayer)](https://GitHub.com/Kingtous/RustPlayer/tags/) [![GitHub stars](https://badgen.net/github/stars/Kingtous/RustPlayer)](https://github.com/Kingtous/RustPlayer/stargazers/) ![build status](https://github.com/Kingtous/RustPlayer/actions/workflows/rust.yml/badge.svg) [![Rust Build, bump tag version and release](https://github.com/Kingtous/RustPlayer/actions/workflows/rust-release.yml/badge.svg)](https://github.com/Kingtous/RustPlayer/actions/workflows/rust-release.yml) +[![rustplayer](https://snapcraft.io/rustplayer/badge.svg)](https://snapcraft.io/rustplayer) +[![rustplayer](https://snapcraft.io/rustplayer/trending.svg?name=0)](https://snapcraft.io/rustplayer) + [![Linux](https://svgshare.com/i/Zhy.svg)](https://svgshare.com/i/Zhy.svg) [![macOS](https://svgshare.com/i/ZjP.svg)](https://svgshare.com/i/ZjP.svg) [![Windows](https://svgshare.com/i/ZhY.svg)](https://svgshare.com/i/ZhY.svg) -[![GitHub stars](https://badgen.net/github/stars/Kingtous/RustPlayer)](https://github.com/Kingtous/RustPlayer/stargazers/) -An local audio player using completely terminal gui. MacOS, Linux, Windows are all supported. +An local audio player & network m3u8 radio player using completely terminal gui. MacOS, Linux, Windows are all supported. RustPlayer is under development. If u have encountered any problem, please open issues :) +## Features + +- Support mp3, wav, flac format +- Support m3u8 network radio + - tested: 央广之声、经济之声. check `radio.ini` for details. + - please copy `radio.ini` to `~/.config/rustplayer` +- Lyrics Supported +- Multi-platform supported +- Low CPU and memory usage +- File explorer +- Playlist playback supported +- Wave animation +- Playback progress +- Next audio +- Adjust volume +- Developed by Kingtous + ## Screenshots ### Windows @@ -33,19 +52,10 @@ The screenshot from Deepin - [Bilibili Video](https://www.bilibili.com/video/BV1T34y1k7Xf) -## Features -- Support mp3, wav, flac format -- Lyrics Supported -- Multi-platform supported -- Low CPU and memory usage -- File explorer -- Playlist playback supported -- Wave animation -- Playback progress -- Next audio -- Adjust volume -- Developed by Kingtous +## Install RustPlayer by Snap Store + +`snap install rustplayer --devmode` ## Download Binary Release Directly and Run @@ -56,6 +66,45 @@ The binary release of macOS, Linux, Windows can be found in artifacts of [RustPl If u found this binary release is not working or u like compiling RustPlayer by youselef. Yes, The step to compile RustPlayer is really easy. - clone this repo. +- install dependencies + - check `.github/rust.yml` for details - `cargo run` in root of this project. if u think this repo is helpful, ⭐ this project and let me know :) + +## TroubleShoot + +### Linux + +Q: No sound in Linux, console shows "unable to open slave". I'm using `snd_hda_intel` drivers. + +A: check your valid sound card. Check by `lspci -knn|grep -iA2 audio`. An example is: +``` +04:00.1 Audio device [0403]: Advanced Micro Devices, Inc. [AMD/ATI] Renoir Radeon High Definition Audio Controller [1002:1637] + Subsystem: Lenovo Device [17aa:3814] + Kernel driver in use: snd_hda_intel +-- +04:00.5 Multimedia controller [0480]: Advanced Micro Devices, Inc. [AMD] ACP/ACP3X/ACP6x Audio Coprocessor [1022:15e2] (rev 01) + Subsystem: Lenovo Device [17aa:3832] + Kernel modules: snd_pci_acp3x, snd_rn_pci_acp3x, snd_pci_acp5x +04:00.6 Audio device [0403]: Advanced Micro Devices, Inc. [AMD] Family 17h/19h HD Audio Controller [1022:15e3] + Subsystem: Lenovo Device [17aa:3833] + Kernel driver in use: snd_hda_intel +``` + +In the case above, 2 audio devices found in your Linux. Let's check which device is in use, we will use `index` to identify the default device. Type `modinfo snd_hda_intel | grep index`, if only shows: + +``` +parm: index:Index value for Intel HD audio interface. (array of int) +``` + +which means index 0 will be chosen to be the default output device. + +In this case, you can try device 1. create files below: +```shell +> cat /etc/modprobe.d/default.conf + +options snd_hda_intel index=1 +``` + +reboot and check if it works. diff --git a/radio.ini b/radio.ini index 3777a81..038601b 100644 --- a/radio.ini +++ b/radio.ini @@ -1 +1,2 @@ -央广新闻 http://ngcdn002.cnr.cn/live/jjzs/index.m3u8 \ No newline at end of file +经济之声 http://ngcdn002.cnr.cn/live/jjzs/index.m3u8 +中国之声 http://ngcdn001.cnr.cn/live/zgzs/index.m3u8 \ No newline at end of file diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml new file mode 100644 index 0000000..cc6ee10 --- /dev/null +++ b/snap/snapcraft.yaml @@ -0,0 +1,51 @@ +name: rustplayer +version: '1.1.0-2' +summary: A local music player && network radio player +description: | + RustPlayer is a local music player && network radio player: + +base: core18 +confinement: strict + +architectures: + - build-on: i386 + - build-on: amd64 + - build-on: arm64 + - build-on: armhf + - build-on: ppc64el + +parts: + rustplayer: + plugin: rust + source: . + build-packages: + - libasound2-dev + - libavcodec-dev + - libavformat-dev + - libswresample-dev + - libavutil-dev + - pkg-config + - libssl-dev + - clang + stage-packages: + - libasound2 + - libavcodec57 + - libavformat57 + - libswresample2 + - libavutil55 + +layout: + /usr/share/alsa: + bind: $SNAP/usr/share/alsa + +apps: + rustplayer: + command: bin/RustPlayer + plugs: + - network + - audio-playback + - home + # connect manually + - pulseaudio + - alsa + - removable-media diff --git a/src/app.rs b/src/app.rs index 342b84e..62c2d92 100644 --- a/src/app.rs +++ b/src/app.rs @@ -50,7 +50,7 @@ use crate::{ music_board::{draw_music_board, MusicController}, radio::{draw_radio_list, RadioExplorer}, EventType, - }, + }, util::m3u8::empty_cache, }; pub enum InputMode { @@ -131,6 +131,7 @@ impl App { match self.mode { InputMode::Normal => match key.code { KeyCode::Char('q') | KeyCode::Char('Q') => { + empty_cache(); break; } code => { diff --git a/src/handler/help.rs b/src/handler/help.rs index 48e88ef..7ee5d9a 100644 --- a/src/handler/help.rs +++ b/src/handler/help.rs @@ -25,6 +25,12 @@ pub fn handle_help(app: &mut App, code: KeyCode) -> bool { open::that(app.config.home_page); return true; } + KeyCode::Char('r') => { + let mut config_dir = dirs::config_dir().unwrap(); + config_dir.push("RustPlayer"); + open::that(config_dir); + return true; + } _ => {return false;} } } \ No newline at end of file diff --git a/src/media/player.rs b/src/media/player.rs index d744ece..7245349 100644 --- a/src/media/player.rs +++ b/src/media/player.rs @@ -15,35 +15,44 @@ // You should have received a copy of the GNU General Public License // along with RustPlayer. If not, see . +use bytes::Bytes; +use std::cmp::max; +use std::fs; +use std::future::Future; +use std::io::Cursor; +use std::sync::mpsc::{Receiver, Sender, TryRecvError}; use std::{ cell::{Ref, RefCell}, fmt::Debug, fs::File, - io::{BufReader, Error}, + io::{BufReader, Error, Write}, ops::Add, path::Path, ptr::null, - sync::{mpsc::{self, channel}, Arc, Mutex}, + sync::{ + mpsc::{self, channel}, + Arc, Mutex, + }, thread, - time::{Duration, Instant}, + time::{Duration, Instant, SystemTime}, }; -use std::future::Future; -use std::io::Cursor; -use std::sync::mpsc::{Receiver, Sender, TryRecvError}; -use bytes::Bytes; use m3u8_rs::{MediaPlaylist, Playlist}; +use rodio::decoder::DecoderError; use rodio::{cpal, source::Delay}; use rodio::{Decoder, Devices, OutputStream, OutputStreamHandle, Sink, Source}; -use rodio::decoder::DecoderError; use tui::widgets::ListState; -use crate::{m3u8::download_m3u8_playlist, util::net::download_as_bytes}; +use crate::net::download; +use crate::util::m3u8::empty_cache; use crate::{ app, util::lyrics::{Lyric, Lyrics}, }; -use crate::net::download; +use crate::{ + m3u8::download_m3u8_playlist, + util::{net::download_as_bytes}, +}; use super::media::Media; @@ -192,7 +201,9 @@ impl Player for MusicPlayer { let (stream, stream_handle) = OutputStream::try_default().unwrap(); self.stream = stream; self.stream_handle = stream_handle; + let volume = self.volume(); self.sink = Sink::try_new(&self.stream_handle).unwrap(); + self.set_volume(volume); self.sink.append(Decoder::new(buf_reader).unwrap()); self.play(); } @@ -313,7 +324,6 @@ impl Player for MusicPlayer { } impl MusicPlayer { - pub fn playing_song(&self) -> Option<&PlayListItem> { return self.play_list.lists.first(); } @@ -407,17 +417,19 @@ pub struct RadioPlayer { stream_handle: OutputStreamHandle, sink: Sink, is_playing: bool, - last_playing_id: Option, + last_playing_id: i32, data_tx: Sender, - data_rx: Receiver + data_rx: Receiver, + elasped: SystemTime, + gap: Duration, } impl Player for RadioPlayer { - fn new() -> Self { - let (stream,handle) = OutputStream::try_default().unwrap(); + let (stream, handle) = OutputStream::try_default().unwrap(); let sink = Sink::try_new(&handle).unwrap(); - let (tx,rx) = channel(); + let (tx, rx) = channel(); + empty_cache(); RadioPlayer { item: None, list: vec![], @@ -425,20 +437,23 @@ impl Player for RadioPlayer { stream_handle: handle, sink, is_playing: false, - last_playing_id: None, + last_playing_id: -1, data_rx: rx, - data_tx: tx + data_tx: tx, + elasped: SystemTime::now(), + gap: Duration::from_secs(5), } } fn add_to_list(&mut self, media: Media, _: bool) -> bool { + self.last_playing_id = -1; let src = media.src; match src { super::media::Source::Http(_) => false, super::media::Source::M3u8(url) => { let (tx, mut rx) = channel(); let m3u8_url = url.url.clone(); - thread::spawn( move || { + thread::spawn(move || { let playlist = download_m3u8_playlist(m3u8_url); tx.send(playlist).unwrap(); }); @@ -446,7 +461,7 @@ impl Player for RadioPlayer { Ok(list) => { if let Ok(playlist) = list { match playlist { - Playlist::MasterPlaylist(_) => {}, + Playlist::MasterPlaylist(_) => {} Playlist::MediaPlaylist(pl) => { let item = RadioItem { list: pl, @@ -487,6 +502,7 @@ impl Player for RadioPlayer { fn pause(&mut self) -> bool { self.sink.pause(); + self.is_playing = false; true } @@ -497,7 +513,7 @@ impl Player for RadioPlayer { } fn get_progress(&self) -> (f32, f32) { - return (0.0,0.0); + return (0.0, 0.0); } fn is_playing(&self) -> bool { @@ -507,20 +523,38 @@ impl Player for RadioPlayer { fn tick(&mut self) { match self.data_rx.try_recv() { Ok(data) => { - let f = File::open("D:\\audio.wav").unwrap(); - // let curser = Cursor::new(data.to_vec()); - let decoder = Decoder::new(BufReader::new(f)); + // let f = File::open("D:\\audio.wav").unwrap(); + let mut cache_dir = dirs::cache_dir().unwrap(); + cache_dir.push("RustPlayer"); + std::fs::create_dir_all(cache_dir.clone()).unwrap(); + let timestamp = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap(); + let fname = timestamp.as_nanos().to_string(); + cache_dir.push(fname.as_str()); + let mut f = File::create(cache_dir.clone()).unwrap(); + f.write_all(data.as_ref()).unwrap(); + let decoder = ffmpeg_decoder::Decoder::open(cache_dir); + // let buffer = BufReader::new(f); + // let decoder = rodio::Decoder::new_mp4(buffer, rodio::decoder::Mp4Type::M4a); // Decoder:: match decoder { Ok(dec) => { self.sink.append(dec); } Err(err) => { - eprintln!("{:?}",err); + eprintln!("{:?}", err); } } } - Err(no_data) => {} + Err(_) => {} + } + // check length + if let Ok(elapsed) = self.elasped.elapsed() { + // 过了gap时间重新拉取m3u8 + if self.is_playing() && elapsed.as_secs() > self.gap.as_secs() { + self.download_and_push(); + } } } @@ -543,31 +577,73 @@ impl Player for RadioPlayer { } impl RadioPlayer { - fn download_and_push(&mut self){ - if self.is_playing { - if self.last_playing_id == None { - // 直接全部下载 - let item = &self.item; - if let Some(radio) = item { - let tx_clone = self.data_tx.clone(); - let index = radio.url.clone().rfind("/").unwrap(); - let base_url = radio.url.as_str()[0..index + 1].to_string(); - let urls: Vec = radio.list.segments.iter().map(|e| { - e.uri.clone() - }).map(|uri| { - base_url.clone() + &uri - }).collect(); - thread::spawn(move || { - for url in urls { - match download_as_bytes(url.as_str(), &tx_clone) { - _ => {} + /// 触发下载 + fn download_and_push(&mut self) { + self.elasped = SystemTime::now(); + // 第一次下载,直接全部下载 + if self.last_playing_id == -1 { + // 直接全部下载 + let item = &self.item; + if let Some(radio) = item { + let index = radio.url.clone().rfind("/").unwrap(); + let base_url = radio.url.as_str()[0..index + 1].to_string(); + let tx_clone = self.data_tx.clone(); + let urls: Vec = radio + .list + .segments + .iter() + .map(|e| e.uri.clone()) + .map(|uri| base_url.clone() + &uri) + .collect(); + self.last_playing_id = + radio.list.media_sequence + max((radio.list.segments.len() - 1) as i32, 0); + thread::spawn(move || { + for url in urls { + match download_as_bytes(url.as_str(), &tx_clone) { + _ => { + // println!("{:?} downloaded",url); } } - }); + } + }); + } + } else { + // 更新playlist列表 + if let Some(radio) = &self.item { + let index = radio.url.clone().rfind("/").unwrap(); + let base_url = radio.url.as_str()[0..index + 1].to_string(); + match download_m3u8_playlist(radio.url.clone()) { + Ok(playlist) => match playlist { + Playlist::MasterPlaylist(_) => todo!(), + Playlist::MediaPlaylist(media_playlist) => { + let seq = media_playlist.media_sequence; + let skip_num = max(self.last_playing_id - seq + 1, 0) as usize; + let segs = &media_playlist.segments[skip_num..]; + // println!("add {:?}",segs); + let urls: Vec = segs + .iter() + .map(|e| e.uri.clone()) + .map(|uri| base_url.clone() + &uri) + .collect(); + self.last_playing_id = + seq + max((media_playlist.segments.len() - 1) as i32, 0); + let tx_clone = self.data_tx.clone(); + thread::spawn(move || { + for url in urls { + match download_as_bytes(url.as_str(), &tx_clone) { + _ => { + // println!("update {:?}",url); + } + } + } + }); + } + }, + Err(_) => { + // ignore + } } - } else { - } } } -} \ No newline at end of file +} diff --git a/src/ui/effects.rs b/src/ui/effects.rs index 7b3719f..120d02f 100644 --- a/src/ui/effects.rs +++ b/src/ui/effects.rs @@ -33,12 +33,13 @@ where B: Backend, { let player = &mut app.player; + let radio = &app.radio; let mut rng = rand::thread_rng(); let mut cols = vec![]; for _ in 0..20 { let mut i = rng.gen_range(0..10); - if !player.is_playing() { + if !player.is_playing() && !radio.is_playing() { i = 0 } cols.push(("_", i)) diff --git a/src/ui/help.rs b/src/ui/help.rs index ed3a83c..60353b4 100644 --- a/src/ui/help.rs +++ b/src/ui/help.rs @@ -44,7 +44,8 @@ where frame.render_widget(homepage_text, chunks[0]); let help_table = Table::new([Row::new(["h", "open or close this help."]), - Row::new(["Tab","switch highlight block. (Explorer/Control Panel)"]), + Row::new(["Tab","switch highlight block. (Audio Explorer/Radio Explorer)"]), + Row::new(["r","open radio config list."]), Row::new(["->","add audio to play list."]), Row::new(["Enter","play audio immediately and clean play list or enter selected folder."]), Row::new(["-/+","decrease/increase volume."]), diff --git a/src/ui/radio.rs b/src/ui/radio.rs index cef46f3..3d8eb51 100644 --- a/src/ui/radio.rs +++ b/src/ui/radio.rs @@ -42,6 +42,9 @@ impl RadioExplorer { let reader = BufReader::new(f); let lines = reader.lines().map(|i| i.unwrap_or("".to_string())); for line in lines { + if line.is_empty() { + continue; + } let radio_bean: Vec<_> = line.split(' ').collect(); let config = RadioConfig { name: radio_bean[0].to_string(), diff --git a/src/util/ffmpeg.rs b/src/util/ffmpeg.rs deleted file mode 100644 index c0c3fab..0000000 --- a/src/util/ffmpeg.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub fn convert_aac_to_wav(){ - -} \ No newline at end of file diff --git a/src/util/m3u8.rs b/src/util/m3u8.rs index affb079..d7421fb 100644 --- a/src/util/m3u8.rs +++ b/src/util/m3u8.rs @@ -1,32 +1,42 @@ -use std::fmt::format; +use std::fs::{File, read_dir}; use std::sync::mpsc; use std::sync::mpsc::RecvTimeoutError; use std::thread; use std::time::Duration; +use std::{fmt::format, fs}; -use failure::{Error, format_err}; +use failure::{format_err, Error}; use m3u8_rs::{MediaPlaylist, Playlist}; use crate::net::{download, DownloadTimeoutError}; pub fn download_m3u8_playlist(url: String) -> Result { let (tx, rx) = mpsc::channel(); - thread::spawn( move || { - download(url.as_str(), &tx) - }); + thread::spawn(move || download(url.as_str(), &tx)); let resp = rx.recv_timeout(Duration::from_secs(5)); return if let Ok(data) = resp { let playlist = m3u8_rs::parse_playlist(data.as_bytes()); match playlist { - Ok(list) => { - Ok(list.1) - } - Err(err) => { - Err(format_err!("Parse Playlist Failed")) - } + Ok(list) => Ok(list.1), + Err(err) => Err(format_err!("Parse Playlist Failed")), } } else { - println!("{:?}",resp); + println!("{:?}", resp); Err(format_err!("Download Timeout in 5 seconds.")) }; -} \ No newline at end of file +} + +pub fn empty_cache() { + let mut dir = dirs::cache_dir().unwrap(); + dir.push("RustPlayer"); + if dir.exists() && dir.is_dir() { + let it = read_dir(dir.clone()); + if let Ok(dirs) = it { + for dir in dirs{ + if let Ok(d) = dir { + std::fs::remove_file(d.path()); + } + } + } + } +} diff --git a/src/util/mod.rs b/src/util/mod.rs index 5f77255..642f234 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -17,5 +17,4 @@ pub mod lyrics; pub mod net; -pub mod m3u8; -pub mod ffmpeg; \ No newline at end of file +pub mod m3u8; \ No newline at end of file diff --git a/thirdparty/ffmpeg-decoder-rs/.gitignore b/thirdparty/ffmpeg-decoder-rs/.gitignore new file mode 100644 index 0000000..02c2f22 --- /dev/null +++ b/thirdparty/ffmpeg-decoder-rs/.gitignore @@ -0,0 +1,6 @@ +/target +**/*.rs.bk +Cargo.lock +.vscode + +assets/*.raw \ No newline at end of file diff --git a/thirdparty/ffmpeg-decoder-rs/Cargo.toml b/thirdparty/ffmpeg-decoder-rs/Cargo.toml new file mode 100644 index 0000000..972475b --- /dev/null +++ b/thirdparty/ffmpeg-decoder-rs/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "ffmpeg-decoder" +version = "0.1.3" +authors = ["tarkah "] +edition = "2018" +license = "MIT" +description = "Decodes audio files using ffmpeg with rust. Can be used as a rodio source." +documentation = "https://docs.rs/ffmpeg-decoder" +repository = "https://github.com/tarkah/ffmpeg-decoder-rs" +readme = "README.md" +keywords = ["audio", "ffmpeg", "rodio"] +categories = ["multimedia::audio", "multimedia::encoding"] + +[lib] +name = "ffmpeg_decoder" +path = "src/lib.rs" + +[features] +default = [] +rodio_source = ['rodio'] + +[dependencies] +ffmpeg-sys-next = { version = "4.4.0", default-features=false, features=["avcodec", "avformat", "swresample"] } + +thiserror = "1.0" +log = "0.4" + +rodio = { version = "0.15.0", default-features=false, optional=true } + +[workspace] +members = [ + ".", + "cli" +] +default-members = [ + "cli", +] diff --git a/thirdparty/ffmpeg-decoder-rs/LICENSE b/thirdparty/ffmpeg-decoder-rs/LICENSE new file mode 100644 index 0000000..87e2b15 --- /dev/null +++ b/thirdparty/ffmpeg-decoder-rs/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 tarkah + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/thirdparty/ffmpeg-decoder-rs/README.md b/thirdparty/ffmpeg-decoder-rs/README.md new file mode 100644 index 0000000..842b2b7 --- /dev/null +++ b/thirdparty/ffmpeg-decoder-rs/README.md @@ -0,0 +1,28 @@ +# ffmpeg-decoder +[![Crates.io](https://img.shields.io/crates/v/ffmpeg-decoder)](https://crates.io/crates/ffmpeg-decoder) +[![](https://docs.rs/ffmpeg-decoder/badge.svg)](https://docs.rs/ffmpeg-decoder) + + +Decodes audio files and converts sample format to signed 16bit. Can +be used as a playback source with [rodio](https://github.com/RustAudio/rodio). + + +## Rodio Source + +`Decoder` implies rodio's `Source` trait, as well as `Iterator`. Enable feature +flag `rodio_source` to include this. Decoder can then be used as a source for Rodio, +with the benefits of being able to decode everything ffmpeg supports. + + +## Testing with CLI + + +### Convert input file to signed 16bit and save as `.raw` alongisde original +``` +cargo run --release -- convert path/to/test.mp3 +``` + +### Play with rodio +``` +cargo run --release -- play path/to/test.flac +``` \ No newline at end of file diff --git a/thirdparty/ffmpeg-decoder-rs/cli/Cargo.toml b/thirdparty/ffmpeg-decoder-rs/cli/Cargo.toml new file mode 100644 index 0000000..13cd01d --- /dev/null +++ b/thirdparty/ffmpeg-decoder-rs/cli/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "ffmpeg-decoder-cli" +version = "0.1.0" +authors = ["tarkah "] +edition = "2018" + +[dependencies] +ffmpeg-decoder = { path = "../", features = ['rodio_source'] } + +rodio = { version = "0.11", default-features=false } + +anyhow = "1.0" +env_logger = "0.7" +log = "0.4" +structopt = "0.3" \ No newline at end of file diff --git a/thirdparty/ffmpeg-decoder-rs/cli/src/main.rs b/thirdparty/ffmpeg-decoder-rs/cli/src/main.rs new file mode 100644 index 0000000..7e5f400 --- /dev/null +++ b/thirdparty/ffmpeg-decoder-rs/cli/src/main.rs @@ -0,0 +1,88 @@ +use anyhow::Error; +use env_logger::Env; +use log::{error, info}; +use structopt::StructOpt; + +use rodio::Sink; + +use std::path::PathBuf; + +fn main() { + env_logger::from_env(Env::default().default_filter_or("info")).init(); + + let opts = Opts::from_args(); + + let status = match opts.command { + Command::Convert { input } => decode_to_file(input), + Command::Play { input } => play_file(input), + }; + + if let Err(e) = status { + log_error(e); + std::process::exit(1); + } +} + +fn decode_to_file(input: PathBuf) -> Result<(), Error> { + let decoder = ffmpeg_decoder::Decoder::open(&input)?; + + let samples = decoder.collect::>(); + + let samples_u8 = + unsafe { std::slice::from_raw_parts(samples.as_ptr() as *const u8, samples.len() * 2) }; + + let mut out_path: PathBuf = input; + out_path.set_extension("raw"); + + std::fs::write(&out_path, samples_u8)?; + + info!( + "File successfully decoded, converted and saved to: {:?}", + out_path + ); + + Ok(()) +} + +fn play_file(input: PathBuf) -> Result<(), Error> { + let decoder = ffmpeg_decoder::Decoder::open(&input)?; + + let device = rodio::default_output_device().unwrap(); + let sink = Sink::new(&device); + + sink.append(decoder); + sink.play(); + sink.sleep_until_end(); + + Ok(()) +} + +fn log_error(e: Error) { + error!("{}", e); +} + +#[derive(StructOpt)] +#[structopt( + name = "libav-decoder-cli", + about = "Convert input audio file sample format to signed 16bit" +)] +struct Opts { + #[structopt(subcommand)] + command: Command, +} + +#[derive(StructOpt)] +enum Command { + /// Convert file and save as `.raw` alongside input file + Convert { + /// Input audio file + #[structopt(parse(from_os_str))] + input: PathBuf, + }, + /// Play input file + Play { + /// Input audio file + #[structopt(parse(from_os_str))] + input: PathBuf, + }, +} diff --git a/thirdparty/ffmpeg-decoder-rs/src/decoder.rs b/thirdparty/ffmpeg-decoder-rs/src/decoder.rs new file mode 100644 index 0000000..c38b5ae --- /dev/null +++ b/thirdparty/ffmpeg-decoder-rs/src/decoder.rs @@ -0,0 +1,621 @@ +use crate::error::Error; + +use ffmpeg_sys_next::{ + self, av_frame_alloc, av_frame_free, av_frame_unref, av_freep, av_get_alt_sample_fmt, + av_get_bytes_per_sample, av_get_channel_layout_nb_channels, av_get_sample_fmt_name, + av_init_packet, av_packet_unref, av_read_frame, av_register_all, av_sample_fmt_is_planar, + av_samples_alloc, av_samples_get_buffer_size, avcodec_alloc_context3, avcodec_close, + avcodec_find_decoder, avcodec_free_context, avcodec_open2, avcodec_parameters_to_context, + avcodec_receive_frame, avcodec_send_packet, avformat_close_input, avformat_find_stream_info, + avformat_open_input, swr_alloc_set_opts, swr_convert, swr_get_out_samples, swr_init, AVCodec, + AVCodecContext, AVFormatContext, AVFrame, AVMediaType, AVPacket, AVSampleFormat, AVStream, +}; +use std::ffi::{CStr, CString}; +use std::path::Path; +use std::ptr; +use std::slice; +use std::time::Duration; + +use log::{error, info}; + +const AVERROR_EOF: i32 = -0x20_464_F45; +const AVERROR_EAGAIN: i32 = -11; +const AVERROR_EDEADLK: i32 = -35; +const DEFAULT_CONVERSION_FORMAT: AVSampleFormat = AVSampleFormat::AV_SAMPLE_FMT_S16; + +pub struct Decoder { + format_ctx: FormatContext, + stream: Stream, + codec_ctx: CodecContext, + frame: Frame, + packet: Packet, + swr_ctx: Option, + current_frame: Vec, + first_frame_stored: bool, +} + +impl Decoder { + pub fn open(path: impl AsRef) -> Result { + unsafe { av_register_all() }; + + // Open the file and get the format context + let format_ctx = FormatContext::open(&path.as_ref().display().to_string())?; + + // Find first audio stream in file + format_ctx.find_stream_info()?; + let stream = format_ctx.get_audio_stream()?; + + // Get the streams codec + let codec = stream.get_codec()?; + + // Setup codec context and intialize + let codec_ctx = codec.get_context()?; + codec_ctx.copy_parameters_from_stream(&stream)?; + codec_ctx.request_non_planar_format(); + codec_ctx.initialize()?; + + print_codec_info(&codec_ctx); + + // Allocate frame + let frame = Frame::new()?; + + // Initialize packet + let packet = Packet::new(); + + // Initialize swr context, if conversion is needed + let swr_ctx = if codec_ctx.sample_format() != DEFAULT_CONVERSION_FORMAT { + Some(SwrContext::new(&codec_ctx)?) + } else { + None + }; + + Ok(Decoder { + format_ctx, + stream, + codec_ctx, + frame, + packet, + swr_ctx, + current_frame: vec![], + first_frame_stored: false, + }) + } + + fn read_next_frame(&mut self) -> ReadFrameStatus { + let status = + unsafe { av_read_frame(self.format_ctx.inner, self.packet.inner.as_mut_ptr()) }; + + match status { + AVERROR_EOF => ReadFrameStatus::Eof, + _ if status != 0 => ReadFrameStatus::Other(status), + _ => ReadFrameStatus::Ok, + } + } + + fn send_packet_for_decoding(&mut self) -> SendPacketStatus { + let status = + unsafe { avcodec_send_packet(self.codec_ctx.inner, self.packet.inner.as_mut_ptr()) }; + + match status { + 0 => SendPacketStatus::Ok, + _ => SendPacketStatus::Other(status), + } + } + + fn receive_decoded_frame(&self) -> ReceiveFrameStatus { + let status = unsafe { avcodec_receive_frame(self.codec_ctx.inner, self.frame.inner) }; + + match status { + 0 => ReceiveFrameStatus::Ok, + AVERROR_EAGAIN => ReceiveFrameStatus::Again, + AVERROR_EDEADLK => ReceiveFrameStatus::Deadlk, + _ => ReceiveFrameStatus::Other(status), + } + } + + fn convert_and_store_frame(&mut self) { + let num_samples = self.frame.num_samples(); + let channel_layout = self.frame.channel_layout(); + let num_channels = unsafe { av_get_channel_layout_nb_channels(channel_layout) }; + + let extended_data = self.frame.extended_data(); + + let mut out_buf = std::ptr::null_mut::(); + + let out_slice = if self.swr_ctx.is_some() { + let out_samples = + unsafe { swr_get_out_samples(self.swr_ctx.as_ref().unwrap().inner, num_samples) }; + + unsafe { + av_samples_alloc( + &mut out_buf, + ptr::null_mut(), + num_channels, + out_samples, + DEFAULT_CONVERSION_FORMAT, + 0, + ) + }; + + unsafe { + swr_convert( + self.swr_ctx.as_ref().unwrap().inner, + &mut out_buf, + out_samples, + extended_data, + num_samples, + ) + }; + + let out_size = unsafe { + av_samples_get_buffer_size( + ptr::null_mut(), + num_channels, + out_samples, + DEFAULT_CONVERSION_FORMAT, + 0, + ) + }; + + unsafe { slice::from_raw_parts(out_buf, out_size as usize) } + } else { + unsafe { + slice::from_raw_parts( + extended_data.as_ref().unwrap().as_ref().unwrap(), + self.frame.inner.as_ref().unwrap().linesize[0] as usize, + ) + } + }; + + if !self.current_frame.is_empty() { + self.current_frame.drain(..); + } + + self.current_frame.extend_from_slice(out_slice); + + if self.swr_ctx.is_some() { + // Free samples buffer + unsafe { av_freep(&mut out_buf as *mut _ as _) }; + } + + unsafe { av_frame_unref(self.frame.inner) }; + } + + fn frame_for_stream(&self) -> bool { + unsafe { self.packet.inner.as_ptr().as_ref().unwrap().stream_index == self.stream.index } + } + + fn reset_packet(&mut self) { + unsafe { av_packet_unref(self.packet.inner.as_mut_ptr()) }; + } + + fn next_sample(&mut self) -> i16 { + let sample_u8: [u8; 2] = [self.current_frame.remove(0), self.current_frame.remove(0)]; + + ((sample_u8[1] as i16) << 8) | sample_u8[0] as i16 + } + + fn process_next_frame(&mut self) -> Option> { + match self.read_next_frame() { + ReadFrameStatus::Ok => {} + ReadFrameStatus::Eof => { + return None; + } + ReadFrameStatus::Other(status) => { + error!("{}", Error::ReadFrame(status)); + return None; + } + } + + if !self.frame_for_stream() { + self.reset_packet(); + return self.process_next_frame(); + } + + match self.send_packet_for_decoding() { + SendPacketStatus::Ok => self.reset_packet(), + SendPacketStatus::Other(status) => { + error!("{}", Error::SendPacket(status)); + return None; + } + } + + match self.receive_decoded_frame() { + ReceiveFrameStatus::Ok => {} + ReceiveFrameStatus::Again | ReceiveFrameStatus::Deadlk => { + return self.process_next_frame() + } + ReceiveFrameStatus::Other(status) => { + error!("{}", Error::ReceiveFrame(status)); + return None; + } + } + + self.convert_and_store_frame(); + + Some(Ok(())) + } + + fn cleanup(&mut self) { + // Drain the decoder. + drain_decoder(self.codec_ctx.inner).unwrap(); + + unsafe { + // Free all data used by the frame. + av_frame_free(&mut self.frame.inner); + + // Close the context and free all data associated to it, but not the context itself. + avcodec_close(self.codec_ctx.inner); + + // Free the context itself. + avcodec_free_context(&mut self.codec_ctx.inner); + + // Close the input. + avformat_close_input(&mut self.format_ctx.inner); + } + } + + pub(crate) fn _current_frame_len(&self) -> Option { + Some(self.current_frame.len()) + } + + pub(crate) fn _channels(&self) -> u16 { + self.codec_ctx.channels() as _ + } + + pub(crate) fn _sample_rate(&self) -> u32 { + self.codec_ctx.sample_rate() as _ + } + + pub(crate) fn _total_duration(&self) -> Option { + //TODO let duration = self.stream.duration(); + None + } +} + +unsafe impl Send for Decoder {} + +impl Iterator for Decoder { + type Item = i16; + + #[inline] + fn next(&mut self) -> Option { + if !self.first_frame_stored { + if self.process_next_frame().is_none() { + self.cleanup(); + return None; + } + + self.first_frame_stored = true; + + return Some(self.next_sample()); + } + + if !self.current_frame.is_empty() { + return Some(self.next_sample()); + } + + match self.receive_decoded_frame() { + ReceiveFrameStatus::Ok => { + self.convert_and_store_frame(); + Some(self.next_sample()) + } + ReceiveFrameStatus::Again | ReceiveFrameStatus::Deadlk => { + if self.process_next_frame().is_none() { + self.cleanup(); + return None; + } + + Some(self.next_sample()) + } + ReceiveFrameStatus::Other(status) => { + error!("{}", Error::ReceiveFrame(status)); + self.cleanup(); + None + } + } + } +} + +struct FormatContext { + inner: *mut AVFormatContext, +} + +impl FormatContext { + fn open(path: &str) -> Result { + let mut inner = std::ptr::null_mut::(); + + let path = CString::new(path).unwrap(); + + let status = unsafe { + avformat_open_input( + &mut inner, + path.as_ptr(), + std::ptr::null_mut(), + std::ptr::null_mut(), + ) + }; + if status != 0 { + return Err(Error::InitializeFormatContext); + } + + Ok(FormatContext { inner }) + } + + /// Look at first few frames to determine stream info + fn find_stream_info(&self) -> Result<(), Error> { + let status = unsafe { avformat_find_stream_info(self.inner, ptr::null_mut()) }; + if status < 0 { + return Err(Error::FindStreamInfo); + } + Ok(()) + } + + /// Get the first audio stream + fn get_audio_stream(&self) -> Result { + let num_streams = unsafe { self.inner.as_ref().unwrap().nb_streams }; + let streams = unsafe { self.inner.as_ref().unwrap().streams }; + + let streams = unsafe { slice::from_raw_parts(streams, num_streams as usize) }; + + let stream_idx = find_audio_stream(streams)?; + + Ok(Stream::new(streams[0], stream_idx)) + } +} + +struct SwrContext { + inner: *mut ffmpeg_sys_next::SwrContext, +} + +impl SwrContext { + fn new(codec_ctx: &CodecContext) -> Result { + let swr_ctx: *mut ffmpeg_sys_next::SwrContext = unsafe { + swr_alloc_set_opts( + ptr::null_mut(), + codec_ctx.channel_layout() as i64, + DEFAULT_CONVERSION_FORMAT, + codec_ctx.sample_rate(), + codec_ctx.channel_layout() as i64, + codec_ctx.sample_format(), + codec_ctx.sample_rate(), + 0, + ptr::null_mut(), + ) + }; + + let status = unsafe { swr_init(swr_ctx) }; + if status != 0 { + return Err(Error::InitializeSwr); + } + + Ok(SwrContext { inner: swr_ctx }) + } +} + +struct Packet { + inner: std::mem::MaybeUninit, +} + +impl Packet { + fn new() -> Packet { + let mut packet = std::mem::MaybeUninit::uninit(); + + unsafe { av_init_packet(packet.as_mut_ptr()) }; + + Packet { inner: packet } + } +} + +struct Frame { + inner: *mut AVFrame, +} + +impl Frame { + fn new() -> Result { + let frame: *mut AVFrame = unsafe { av_frame_alloc() }; + + if frame.is_null() { + return Err(Error::NullFrame); + } + + Ok(Frame { inner: frame }) + } + + fn num_samples(&self) -> i32 { + unsafe { self.inner.as_ref().unwrap().nb_samples } + } + + fn channel_layout(&self) -> u64 { + unsafe { self.inner.as_ref().unwrap().channel_layout } + } + + fn extended_data(&self) -> *mut *const u8 { + unsafe { self.inner.as_ref().unwrap().extended_data as *mut *const u8 } + } +} + +struct Stream { + inner: *mut AVStream, + index: i32, +} + +impl Stream { + fn new(inner: *mut AVStream, index: i32) -> Stream { + Stream { inner, index } + } + + fn get_codec(&self) -> Result { + // Get streams codec + let codec_params = unsafe { self.inner.as_ref().unwrap().codecpar }; + let codec_id = unsafe { codec_params.as_ref().unwrap().codec_id }; + + let codec: *mut AVCodec = unsafe { avcodec_find_decoder(codec_id) }; + if codec.is_null() { + return Err(Error::NullCodec); + } + + Ok(Codec::new(codec)) + } + + #[allow(dead_code)] + fn duration(&self) -> i64 { + unsafe { self.inner.as_ref().unwrap().duration } + } +} + +struct CodecContext { + inner: *mut AVCodecContext, + codec: *mut AVCodec, +} + +impl CodecContext { + fn new(inner: *mut AVCodecContext, codec: *mut AVCodec) -> CodecContext { + CodecContext { inner, codec } + } + + fn copy_parameters_from_stream(&self, stream: &Stream) -> Result<(), Error> { + let params = unsafe { stream.inner.as_ref().unwrap().codecpar }; + + let status = unsafe { avcodec_parameters_to_context(self.inner, params) }; + + if status != 0 { + return Err(Error::CodecParamsToContext); + } + + Ok(()) + } + + fn request_non_planar_format(&self) { + unsafe { + let sample_fmt = self.inner.as_ref().unwrap().sample_fmt; + let alt_format = av_get_alt_sample_fmt(sample_fmt, 0); + + self.inner.as_mut().unwrap().request_sample_fmt = alt_format; + } + } + + fn initialize(&self) -> Result<(), Error> { + let status = unsafe { avcodec_open2(self.inner, self.codec, &mut std::ptr::null_mut()) }; + + if status != 0 { + return Err(Error::InitializeDecoder); + } + + Ok(()) + } + + fn codec_name(&self) -> &str { + let name = unsafe { CStr::from_ptr(self.codec.as_ref().unwrap().long_name) }; + + name.to_str().unwrap() + } + + fn sample_format(&self) -> AVSampleFormat { + unsafe { self.inner.as_ref().unwrap().sample_fmt } + } + + fn sample_format_name(&self) -> &str { + let sample_fmt = unsafe { CStr::from_ptr(av_get_sample_fmt_name(self.sample_format())) }; + + sample_fmt.to_str().unwrap() + } + + fn sample_rate(&self) -> i32 { + unsafe { self.inner.as_ref().unwrap().sample_rate } + } + + fn sample_size(&self) -> i32 { + unsafe { av_get_bytes_per_sample(self.inner.as_ref().unwrap().sample_fmt) } + } + + fn channels(&self) -> i32 { + unsafe { self.inner.as_ref().unwrap().channels } + } + + fn channel_layout(&self) -> u64 { + unsafe { self.inner.as_ref().unwrap().channel_layout } + } + + fn is_planar(&self) -> i32 { + unsafe { av_sample_fmt_is_planar(self.inner.as_ref().unwrap().sample_fmt) } + } +} + +struct Codec { + inner: *mut AVCodec, +} + +impl Codec { + fn new(inner: *mut AVCodec) -> Codec { + Codec { inner } + } + + fn get_context(&self) -> Result { + let ctx: *mut AVCodecContext = unsafe { avcodec_alloc_context3(self.inner) }; + + if ctx.is_null() { + return Err(Error::NullCodecContext); + } + + Ok(CodecContext::new(ctx, self.inner)) + } +} + +enum ReadFrameStatus { + Ok, + Eof, + Other(i32), +} + +enum SendPacketStatus { + Ok, + Other(i32), +} + +enum ReceiveFrameStatus { + Ok, + Again, + Deadlk, + Other(i32), +} + +fn find_audio_stream(streams: &[*mut AVStream]) -> Result { + for stream in streams { + let codec_type = unsafe { + stream + .as_ref() + .unwrap() + .codecpar + .as_ref() + .unwrap() + .codec_type + }; + let index = unsafe { stream.as_ref().unwrap().index }; + + if codec_type == AVMediaType::AVMEDIA_TYPE_AUDIO { + return Ok(index); + } + } + + Err(Error::NoAudioStream) +} + +fn print_codec_info(codec_ctx: &CodecContext) { + info!("Codec: {}", codec_ctx.codec_name()); + info!("Sample Format: {}", codec_ctx.sample_format_name()); + info!("Sample Rate: {}", codec_ctx.sample_rate()); + info!("Sample Size: {}", codec_ctx.sample_size()); + info!("Channels: {}", codec_ctx.channels()); + info!("Planar: {}", codec_ctx.is_planar()); +} + +fn drain_decoder(codec_ctx: *mut AVCodecContext) -> Result<(), Error> { + let status = unsafe { avcodec_send_packet(codec_ctx, std::ptr::null()) }; + if status == 0 { + } else { + return Err(Error::DrainDecoder(status)); + } + + Ok(()) +} diff --git a/thirdparty/ffmpeg-decoder-rs/src/error.rs b/thirdparty/ffmpeg-decoder-rs/src/error.rs new file mode 100644 index 0000000..8445f9d --- /dev/null +++ b/thirdparty/ffmpeg-decoder-rs/src/error.rs @@ -0,0 +1,31 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum Error { + #[error("Failed to initialize format context")] + InitializeFormatContext, + #[error("Could not find stream in file")] + FindStreamInfo, + #[error("Could not find any audio stream")] + NoAudioStream, + #[error("Null codec pointer")] + NullCodec, + #[error("Null codec context pointer")] + NullCodecContext, + #[error("Copying params to codec context")] + CodecParamsToContext, + #[error("Failed to initialize decoder")] + InitializeDecoder, + #[error("Null frame pointer")] + NullFrame, + #[error("Error reading frame: {0}")] + ReadFrame(i32), + #[error("Error sending packet: {0}")] + SendPacket(i32), + #[error("Error draining decoder: {0}")] + DrainDecoder(i32), + #[error("Error receiving frame: {0}")] + ReceiveFrame(i32), + #[error("Failed to initialize swr context")] + InitializeSwr, +} diff --git a/thirdparty/ffmpeg-decoder-rs/src/lib.rs b/thirdparty/ffmpeg-decoder-rs/src/lib.rs new file mode 100644 index 0000000..55ddf19 --- /dev/null +++ b/thirdparty/ffmpeg-decoder-rs/src/lib.rs @@ -0,0 +1,39 @@ +//! Decodes audio files using ffmpeg bindings +//! +//! Create a [`Decoder`](struct.Decoder.html) by supplying a `Path` to an audio file. [`Decoder`](struct.Decoder.html) +//! implies `Iterator` where each iteration returns a single `i16` signed 16bit sample. +//! Also implements [rodio's](https://github.com/RustAudio/rodio) [`Source`](https://docs.rs/rodio/latest/rodio/source/trait.Source.html) trait, where +//! the [`Decoder`](struct.Decoder.html) can be supplied as a sink source for playback. +//! +//! ### Features Flags +//! +//! - `rodio_source` to enable rodio's [`Source`](https://docs.rs/rodio/latest/rodio/source/trait.Source.html) trait +//! +//! +//! ## Example as Rodio Source +//! +//! ```rust +//! use rodio::Sink; +//! use std::path::PathBuf; +//! +//! fn play_file(input: PathBuf) -> Result<(), Error> { +//! let decoder = ffmpeg_decoder::Decoder::open(&input)?; +//! +//! let device = rodio::default_output_device().unwrap(); +//! let sink = Sink::new(&device); +//! +//! sink.append(decoder); +//! sink.play(); +//! sink.sleep_until_end(); +//! +//! Ok(()) +//! } +//! ``` +mod decoder; +pub use decoder::Decoder; + +mod error; +pub use error::Error; + +#[cfg(feature = "rodio_source")] +mod rodio; diff --git a/thirdparty/ffmpeg-decoder-rs/src/rodio.rs b/thirdparty/ffmpeg-decoder-rs/src/rodio.rs new file mode 100644 index 0000000..8cbcca5 --- /dev/null +++ b/thirdparty/ffmpeg-decoder-rs/src/rodio.rs @@ -0,0 +1,27 @@ +use crate::Decoder; + +use rodio::source::Source; + +use std::time::Duration; + +impl Source for Decoder { + #[inline] + fn current_frame_len(&self) -> Option { + self._current_frame_len() + } + + #[inline] + fn channels(&self) -> u16 { + self._channels() + } + + #[inline] + fn sample_rate(&self) -> u32 { + self._sample_rate() + } + + #[inline] + fn total_duration(&self) -> Option { + self._total_duration() + } +}