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 [](https://GitHub.com/Kingtous/RustPlayer/tags/)
+# RustPlayer [](https://GitHub.com/Kingtous/RustPlayer/tags/) [](https://github.com/Kingtous/RustPlayer/stargazers/)

[](https://github.com/Kingtous/RustPlayer/actions/workflows/rust-release.yml)
+[](https://snapcraft.io/rustplayer)
+[](https://snapcraft.io/rustplayer)
+
[](https://svgshare.com/i/Zhy.svg)
[](https://svgshare.com/i/ZjP.svg)
[](https://svgshare.com/i/ZhY.svg)
-[](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
+[](https://crates.io/crates/ffmpeg-decoder)
+[](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()
+ }
+}