tesselode / kira

Library for expressive game audio.
https://crates.io/crates/kira
Apache License 2.0
836 stars 42 forks source link

Infinite media source support #67

Open fragsalat opened 7 months ago

fragsalat commented 7 months ago

Hey there,

first of all thanks for the great work. This library is core of so many playback libraries! To my issue, I want to play a radio stream using Kira. The stream is remote and has no content-length as it is infinite. The issue I recognize is that Kira's SymphoniaDecoder::new function tries to determine sample_rate and num_frames at decoder creation and unfortunately it isn't able to determine the num_frames. I guess this is from Kira targeting game development whose would always have files on disk. Could you maybe consider to also allow playing remote files as well? I really like this library and would like to prevent using rodio for streaming and kira for local files (rodio doesn't support seeking or chaning positions).

Here is my example

use std::io::{Read, Seek, SeekFrom};
use std::thread::sleep;
use std::time::Duration;

use kira::manager::{backend::DefaultBackend, AudioManager, AudioManagerSettings};
use kira::sound::streaming::{StreamingSoundData, StreamingSoundSettings};
use reqwest::{Client, Url};
use stream_download::http::HttpStream;
use stream_download::source::SourceStream;
use stream_download::storage::memory::MemoryStorageProvider;
use stream_download::{Settings, StreamDownload};
use symphonia::core::io::MediaSource;

struct RemoteSource {
    reader: StreamDownload<MemoryStorageProvider>,
    content_length: Option<u64>,
}

impl RemoteSource {
    pub async fn from_url(url: String) -> Result<Self, String> {
        let parsed_url = url.parse::<Url>().map_err(|error| format!("Invalid url: {}", error))?;
        let stream = HttpStream::<Client>::create(parsed_url)
            .await
            .map_err(|error| format!("Failed to create stream: {}", error))?;

        let content_length = stream.content_length();
        let reader = StreamDownload::from_stream(stream, MemoryStorageProvider::default(), Settings::default())
            .await
            .map_err(|error| format!("Failed to create download: {}", error))?;

        Ok(RemoteSource { reader, content_length })
    }
}

impl Read for RemoteSource {
    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
        self.reader.read(buf)
    }
}

impl Seek for RemoteSource {
    fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
        self.reader.seek(pos)
    }
}

impl MediaSource for RemoteSource {
    fn is_seekable(&self) -> bool {
        self.content_length.is_some()
    }

    fn byte_len(&self) -> Option<u64> {
        self.content_length
    }
}

const FILE: &'static str = "H:\\Data\\projects\\mupibox-rs\\admin_interface\\.storage\\audio\\0b7678f1-4121-4d38-be75-c26f86e2e30d-IsItReal.mp3";

#[tokio::main]
async fn main() {
    let mut manager = AudioManager::<DefaultBackend>::new(AudioManagerSettings::default()).expect("manager to load");

    let url = "https://streamtdy.ir-media-tec.com/live/mp3-128/web/play.mp3".to_string();

    let sound = StreamingSoundData::from_media_source(
        RemoteSource::from_url(url).await.expect("source to be loaded"),
        StreamingSoundSettings::default(),
    )
    .expect("stream sound data to be created");

    let mut handle = manager.play(sound).expect("failed to play");

    sleep(Duration::from_secs(5));
}

With these dependencies

[dependencies]
kira = "0.8.5"
symphonia = "0.5.3"
reqwest = { version = "0.11.22", features = ["blocking"] }
stream-download = "0.3.0"
tokio = { version = "1.33.0", features = ["full"] }
rangemap = "1.4.0"

If not I can understand that as well :)

Have a nice day and best regards fragsalat