Skip to content

Commit

Permalink
recorder: initial file recording support
Browse files Browse the repository at this point in the history
Needs metadata variables to really be useful, but a good start
  • Loading branch information
2bc4 committed Feb 20, 2024
1 parent ebc6b34 commit 2fd4353
Show file tree
Hide file tree
Showing 8 changed files with 145 additions and 18 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,13 @@ Almost every option can also be set via config file. Example config file with al
# This is a comment
player=../mpv/mpv
player-args=- --profile=low-latency
record=./recording
servers=https://eu.luminous.dev/live/[channel],https://lb-eu.cdn-perfprod.com/live/[channel]
debug=true
quiet=true
passthrough=false
print-streams=false
overwrite=false
no-low-latency=false
no-kill=false
force-https=true
Expand Down
7 changes: 6 additions & 1 deletion src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ use std::{env, error::Error, fmt::Display, fs, path::Path, process, str::FromStr
use anyhow::{Context, Result};
use pico_args::Arguments;

use crate::{constants, hls::Args as HlsArgs, http::Args as HttpArgs, player::Args as PlayerArgs};
use crate::{
constants, hls::Args as HlsArgs, http::Args as HttpArgs, player::Args as PlayerArgs,
recorder::Args as RecorderArgs,
};

pub trait ArgParser {
fn parse(&mut self, parser: &mut Parser) -> Result<()>;
Expand All @@ -13,6 +16,7 @@ pub trait ArgParser {
pub struct Args {
pub http: HttpArgs,
pub player: PlayerArgs,
pub recorder: RecorderArgs,
pub hls: HlsArgs,
pub debug: bool,
pub passthrough: bool,
Expand All @@ -28,6 +32,7 @@ impl ArgParser for Args {

self.http.parse(parser)?;
self.player.parse(parser)?;
self.recorder.parse(parser)?;
self.hls.parse(parser)?;

if !self.print_streams {
Expand Down
43 changes: 43 additions & 0 deletions src/combinedwriter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
use std::io::{self, Write};

use anyhow::{ensure, Result};

use crate::{player::Player, recorder::Recorder};

pub struct CombinedWriter {
player: Option<Player>,
recorder: Option<Recorder>,
}

impl Write for CombinedWriter {
fn write(&mut self, _buf: &[u8]) -> io::Result<usize> {
unimplemented!()
}

fn flush(&mut self) -> io::Result<()> {
Ok(())
}

fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
if let Some(ref mut player) = self.player {
player.write_all(buf)?;
}

if let Some(ref mut recorder) = self.recorder {
recorder.write_all(buf)?;
}

Ok(())
}
}

impl CombinedWriter {
pub fn new(player: Option<Player>, recorder: Option<Recorder>) -> Result<Self> {
ensure!(
player.is_some() || recorder.is_some(),
"Player or recording must be set"
);

Ok(Self { player, recorder })
}
}
6 changes: 5 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
mod args;
mod combinedwriter;
mod constants;
mod hls;
mod http;
mod logger;
mod player;
mod recorder;
mod worker;

use std::{
Expand All @@ -15,13 +17,15 @@ use anyhow::Result;
use log::{debug, info};

use args::Args;
use combinedwriter::CombinedWriter;
use hls::{
playlist::{MasterPlaylist, MediaPlaylist},
segment::Handler,
};
use http::Agent;
use logger::Logger;
use player::Player;
use recorder::Recorder;
use worker::Worker;

fn main_loop(mut handler: Handler) -> Result<()> {
Expand Down Expand Up @@ -64,7 +68,7 @@ fn main() -> Result<()> {

let mut playlist = MediaPlaylist::new(variant_playlist.url, &agent)?;
let worker = Worker::spawn(
Player::spawn(&args.player)?,
CombinedWriter::new(Player::spawn(&args.player)?, Recorder::new(&args.recorder)?)?,
playlist.header.take(),
agent.clone(),
)?;
Expand Down
28 changes: 17 additions & 11 deletions src/player.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use std::{
process::{Child, ChildStdin, Command, Stdio},
};

use anyhow::{ensure, Context, Result};
use anyhow::{bail, Context, Result};
use log::{debug, error, info};

use crate::args::{ArgParser, Parser};
Expand All @@ -23,7 +23,7 @@ impl Display for PipeClosedError {
#[derive(Clone, Debug)]
#[allow(clippy::struct_field_names)] //.args
pub struct Args {
path: String,
path: Option<String>,
args: String,
quiet: bool,
no_kill: bool,
Expand All @@ -33,7 +33,7 @@ impl Default for Args {
fn default() -> Self {
Self {
args: "-".to_owned(),
path: String::default(),
path: Option::default(),
quiet: bool::default(),
no_kill: bool::default(),
}
Expand All @@ -42,12 +42,11 @@ impl Default for Args {

impl ArgParser for Args {
fn parse(&mut self, parser: &mut Parser) -> Result<()> {
parser.parse_cfg(&mut self.path, "-p", "player")?;
parser.parse_fn_cfg(&mut self.path, "-p", "player", Parser::parse_opt_string)?;
parser.parse_cfg(&mut self.args, "-a", "player-args")?;
parser.parse_switch_or(&mut self.quiet, "-q", "--quiet")?;
parser.parse_switch(&mut self.no_kill, "--no-kill")?;

ensure!(!self.path.is_empty(), "Player must be set");
Ok(())
}
}
Expand Down Expand Up @@ -90,9 +89,13 @@ impl Write for Player {
}

impl Player {
pub fn spawn(pargs: &Args) -> Result<Self> {
info!("Opening player: {} {}", pargs.path, pargs.args);
let mut command = Command::new(&pargs.path);
pub fn spawn(pargs: &Args) -> Result<Option<Self>> {
let Some(ref path) = pargs.path else {
return Ok(None);
};

info!("Opening player: {} {}", path, pargs.args);
let mut command = Command::new(path);
command
.args(pargs.args.split_whitespace())
.stdin(Stdio::piped());
Expand All @@ -107,11 +110,11 @@ impl Player {
.take()
.context("Failed to open player stdin")?;

Ok(Self {
Ok(Some(Self {
stdin,
process,
no_kill: pargs.no_kill,
})
}))
}

pub fn passthrough(pargs: &mut Args, url: &str) -> Result<()> {
Expand All @@ -133,7 +136,10 @@ impl Player {
pargs.args += &format!(" {url}");
}

let mut player = Self::spawn(pargs)?;
let Some(mut player) = Self::spawn(pargs)? else {
bail!("No player set");
};

player
.process
.wait()
Expand Down
63 changes: 63 additions & 0 deletions src/recorder.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
use std::{
fs::File,
io::{self, Write},
};

use anyhow::Result;
use log::{debug, info};

use crate::args::{ArgParser, Parser};

#[derive(Default, Debug)]
pub struct Args {
path: Option<String>,
overwrite: bool,
}

impl ArgParser for Args {
fn parse(&mut self, parser: &mut Parser) -> Result<()> {
parser.parse_fn_cfg(&mut self.path, "-r", "record", Parser::parse_opt_string)?;
parser.parse_switch(&mut self.overwrite, "--overwrite")?;

Ok(())
}
}

pub struct Recorder {
file: File,
}

impl Write for Recorder {
fn write(&mut self, _buf: &[u8]) -> io::Result<usize> {
unimplemented!();
}

fn flush(&mut self) -> io::Result<()> {
debug!("Finished writing segment");
Ok(())
}

fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
self.file.write_all(buf)
}
}

impl Recorder {
pub fn new(args: &Args) -> Result<Option<Self>> {
if let Some(ref path) = args.path {
info!("Recording to {path}");

if args.overwrite {
return Ok(Some(Self {
file: File::create(path)?,
}));
}

return Ok(Some(Self {
file: File::options().write(true).create_new(true).open(path)?,
}));
}

Ok(None)
}
}
6 changes: 5 additions & 1 deletion src/usage
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Usage: twitch-hls-client [OPTIONS] -p <PATH> <CHANNEL> <QUALITY>
Usage: twitch-hls-client [OPTIONS] [-p <PATH> -r <PATH>] <CHANNEL> <QUALITY>

Arguments:
<CHANNEL>
Expand All @@ -11,6 +11,8 @@ Options:
Path to player
-a <ARGUMENTS>
Arguments to pass to the player [default: -]
-r <PATH>
Record to <PATH>
-s <URL1,URL2>
Ad blocking playlist proxy server to fetch the master playlist from.
If not specified will fetch the master playlist directly from Twitch.
Expand All @@ -28,6 +30,8 @@ Options:
Passthrough playlist URL to player and do nothing else
--print-streams
Print available stream qualities and exit
--overwrite
Allow overwriting file when recording
--no-low-latency
Disable low latency streaming
--no-config
Expand Down
8 changes: 4 additions & 4 deletions src/worker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ use anyhow::{ensure, Context, Result};
use log::debug;

use crate::{
combinedwriter::CombinedWriter,
http::{Agent, Url},
player::Player,
};

struct ChannelMessage {
Expand All @@ -25,7 +25,7 @@ pub struct Worker {
}

impl Worker {
pub fn spawn(player: Player, header_url: Option<Url>, agent: Agent) -> Result<Self> {
pub fn spawn(writer: CombinedWriter, header_url: Option<Url>, agent: Agent) -> Result<Self> {
let (url_tx, url_rx): (Sender<ChannelMessage>, Receiver<ChannelMessage>) = mpsc::channel();
let (sync_tx, sync_rx): (SyncSender<()>, Receiver<()>) = mpsc::sync_channel(1);

Expand All @@ -40,12 +40,12 @@ impl Worker {
};

let request = if let Some(header_url) = header_url {
let mut request = agent.writer(player, header_url)?;
let mut request = agent.writer(writer, header_url)?;
request.call(initial_msg.url)?;

request
} else {
agent.writer(player, initial_msg.url)?
agent.writer(writer, initial_msg.url)?
};

if initial_msg.should_sync {
Expand Down

0 comments on commit 2fd4353

Please sign in to comment.