Skip to content

谈谈 Iori 的设计思路(二):如何实现一个 Showroom 录制工具?

Published: at 23:40

大家好,好久不见,我是某昨。

iori 的一个重要设计目标是能够提供简单易用的下载核心,方便各种下载器遍地开花。在 iori 诞生后,出现了 iori-minyamishiori 两个下载工具。这两个工具都是相对通用的下载器,可以满足绝大多数的 HLS 下载需求。但当我们需要进行一些定制时,则需要回到 iori 这个下载核心本身。

由于相对完善的设计,因此直接使用 iori 进行定制下载也非常简单。这里我们就用 Showroom 下载为例,看看这样的一个定制需求要如何实现吧(

ToC

描述一下

首先来描述一下需求。

我们需要的是一个简单的 Showroom 下载工具。它的功能是监听复数个 Showroom 房间的开播情况,并且一旦房间开始直播,就将直播内容进行一个录制。
由于服务器的磁盘空间有限,而带宽和流量相对较足,因此我们希望可以将下载得到的 ts 块直接上传到指定的 S3(Cloudflare R2) 桶中。
Showroom 的录制中可能会出现各种奇怪的错误,具体可以参考院士的《Showroom直播系统录制器的坑与解决方案》。

分析一下

iori 将下载的过程抽象为了 SourceCacheMerger 三部分,Downloader 分别调用这三个部分进行下载。

source 层我们可以直接复用 hls::CommonM3u8LiveSource。虽然一些细节上可能会有些问题,但大部分场景还是可以满足的。
由于我们是希望将 ts 块直接上传到 S3,因此我们可以直接将 S3 作为这个下载器的 Cacheiori 最近引入了 opendal 的支持,因此可以非常轻松地将下载得到的块保存到 S3 中。
而最后的 Merger,由于我们只需要 ts 分块,因此 merger 对这个场景其实是不需要的。

实现一下

至此,我们已经理清了这个下载器需要的所有能力究竟要调用哪部分模块,接下来就是积木时间——

async fn record_room(
client: ShowRoomClient,
room_slug: &str,
room_id: u64,
operator: Operator,
) -> anyhow::Result<()> {
log::debug!("Attempt to record room {room_slug}, id = {room_id}");
let room_info = client.room_profile(room_id).await?;
if !room_info.is_live() {
log::debug!("Room {room_slug} is not live, skipping...");
return Ok(());
}
let stream = client.live_streaming_url(room_id).await?;
let Some(stream) = stream.best(false) else {
log::debug!("Room {room_slug} is not live, skipping...");
return Ok(());
};
let live_id = room_info.live_id;
let live_started_at = chrono::DateTime::from_timestamp(room_info.current_live_started_at, 0)
.unwrap()
.with_timezone(&chrono_tz::Asia::Tokyo)
.to_rfc3339();
let prefix = format!("{room_slug}/{live_id}_{live_started_at}");
let client = HttpClient::default();
let source = CommonM3u8LiveSource::new(client, stream.url.clone(), None, None);
let cache = IoriCache::opendal(operator.clone(), prefix, false);
let merger = IoriMerger::skip();
log::info!("Start recording room {room_slug}, id = {room_id}, live_id = {live_id}");
ParallelDownloaderBuilder::new()
.cache(cache)
.merger(merger)
.download(source)
.await?;
Ok(())
}

简单解释一下。L9-L19 都是在获取 Showroom 的 m3u8 地址;L21-L26 定义了 ts 块在 S3 中的前缀;L28-L31 则是分别定义了上面提到的 SourceCacheMerger;最后 L34-L38 构造 Downloader 下载就可以啦(

完整的代码可以参考: https://github.com/Yesterday17/iori/blob/master/bin/srr/src/main.rs


Previous Post
谈谈 Iori 的设计思路(一):从 Nico Timeshift 说起