Skip to content

Commit

Permalink
feat: web video player
Browse files Browse the repository at this point in the history
  • Loading branch information
nintha committed May 28, 2021
1 parent 96cd930 commit 3acbedc
Show file tree
Hide file tree
Showing 5 changed files with 181 additions and 6 deletions.
22 changes: 17 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ FLAGS:
-V, --version Prints version information
OPTIONS:
--http-flv-port <http-flv-port> disable if port is 0 [default: 0]
--rtmp-port <rtmp-port> [default: 1935]
--ws-fmp4-port <ws-fmp4-port> disable if port is 0 [default: 0]
--ws-h264-port <ws-h264-port> disable if port is 0 [default: 0]
--http-flv-port <http-flv-port> disabled if port is 0 [default: 0]
--http-player-port <http-player-port> disabled if port is 0 [default: 0]
--rtmp-port <rtmp-port> [default: 1935]
--ws-fmp4-port <ws-fmp4-port> disabled if port is 0 [default: 0]
--ws-h264-port <ws-h264-port> disabled if port is 0 [default: 0]
```
## Push

Expand All @@ -35,6 +36,17 @@ If pushing stream with x264 codec, recommended profile is baseline

If you are using x264 encoding to push the stream, it is recommended that profile=baseline to avoid frequent video jitter. The current local test latency is about 1 second.

**Example:**

1. Run `River`
```shell
cargo run -- --http-player-port=8080 --ws-h264-port=18000
```

2. Push with OBS, x264, tune=zerolatency, CBR, preset=veryfast, profile=baseline

3. Open your browser http://localhost:8080

## Completed
- [x] support custom width and height
- [x] support audio
Expand All @@ -43,11 +55,11 @@ If you are using x264 encoding to push the stream, it is recommended that profil
- [x] deal with the problem of websocket message backlog
- [x] configurable startup parameters (monitoring server port)
- [x] optional output formats based on the startup parameters
- [x] web video player with `JMuxer` (ws-h264-port required)

## TODO
- [ ] PUSH/PULL authentication
- [ ] support fragmented MP4 output
- [ ] web video player with `JMuxer` (ws-h264-port required)

## FAQ

Expand Down
39 changes: 39 additions & 0 deletions src/http_player.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
use smol::io::{AsyncWriteExt, AsyncReadExt};
use smol::net::{TcpListener, TcpStream};
use smol::stream::StreamExt;

use crate::util::spawn_and_log_error;

pub async fn run_server(addr: String, player_html: String) -> anyhow::Result<()> {
// Open up a TCP connection and create a URL.
let listener = TcpListener::bind(addr).await?;
let addr = format!("http://{}", listener.local_addr()?);
log::info!("HTTP-Player Server is listening to {}", addr);

// For each incoming TCP connection, spawn a task and call `accept`.
let mut incoming = listener.incoming();
while let Some(stream) = incoming.next().await {
let stream = stream?;
spawn_and_log_error(accept(stream, player_html.clone()));
}
Ok(())
}

async fn accept(mut stream: TcpStream, player_html: String) -> anyhow::Result<()> {
log::info!("[HTTP] new connection from {}", stream.peer_addr()?);

let mut buffer = [0; 1024];
stream.read(&mut buffer).await?;

let header = format!("HTTP/1.1 200 OK\r\n\
Content-Type: text/html;charset=UTF-8\r\n\
Connection: close\r\n\
Content-Length: {}\r\n\
Cache-Control: no-cache\r\n\
Access-Control-Allow-Origin: *\r\n\
\r\n\
{}", player_html.len(), player_html);
stream.write_all(header.as_bytes()).await?;
stream.flush().await?;
Ok(())
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ extern crate num_derive;

mod eventbus;
pub mod http_flv;
pub mod http_player;
pub mod protocol;
pub mod rtmp_server;
pub mod util;
Expand Down
10 changes: 9 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
use clap::crate_version;
use clap::Clap;
use river::{ws_h264, ws_fmp4, util, http_flv};
use river::{ws_h264, ws_fmp4, util, http_flv, http_player};
use river::rtmp_server::accept_loop;
use river::util::spawn_and_log_error;


#[derive(Clap, Debug)]
#[clap(version = crate_version ! (), author = "Ninthakeey <ninthakeey@hotmail.com>")]
struct Opts {
#[clap(long, default_value = "0", about = "disabled if port is 0")]
http_player_port: u16,
#[clap(long, default_value = "0", about = "disabled if port is 0")]
http_flv_port: u16,
#[clap(long, default_value = "0", about = "disabled if port is 0")]
Expand All @@ -25,6 +27,12 @@ fn main() -> anyhow::Result<()> {
let opts: Opts = Opts::parse();
log::info!("{:?}", &opts);

let player_html = include_str!("../static/player.html");
let player_html = player_html.replace("{/*$INJECTED_CONTEXT*/}", &format!("{{port: {}}}", opts.ws_h264_port));

if opts.http_player_port > 0 {
spawn_and_log_error(http_player::run_server(format!("0.0.0.0:{}", opts.http_player_port), player_html));
}
if opts.http_flv_port > 0 {
spawn_and_log_error(http_flv::run_server(format!("0.0.0.0:{}", opts.http_flv_port)));
}
Expand Down
115 changes: 115 additions & 0 deletions static/player.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
<!doctype html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/semantic-ui@2.4.2/dist/semantic.min.css">
<script src="https://cdn.jsdelivr.net/npm/jquery@3.2.1/dist/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/semantic-ui@2.4.2/dist/semantic.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/jmuxer@2.0.2/dist/jmuxer.min.js"></script>
<title>River Player</title>
</head>
<body>
<div id="container" style="margin: 10px auto 0; width: 1024px;">
<video style="border: 1px solid #333; width: 1024px;" autoplay id="player"></video>
<br>
<div>
<button class="ui labeled icon button" onclick="open_ws(url, jmuxer);">
<i class="play icon"></i>
Play
</button>

<button class="ui labeled icon button" onclick="close_ws()">
<i class="stop icon"></i>
Stop
</button>

<button class="ui labeled icon disabled button" onclick="">
<i class="expand icon"></i>
Expand
</button>
</div>
</div>
</body>
<script>
const ctx = {/*$INJECTED_CONTEXT*/};

const url = `ws://${document.domain}:${ctx.port}/websocket${window.location.pathname}`;
let jmuxer = null;
let timer_id = null;

let close_ws = () => {
};

$(function main() {
jmuxer = new JMuxer({
flushingTime: 100,
fps: 30,
node: 'player',
mode: 'both', /* available values are: both, audio and video */
debug: false
});

let player = video_element = document.getElementById('player');

document.addEventListener("visibilitychange", function () {
forward_latest_frame(player);
});

timer_id = setInterval(() => {
forward_latest_frame(player);
}, 2000);
});

function open_ws(url, jmuxer) {
close_ws();

const socket = new WebSocket(url);
socket.binaryType = 'arraybuffer';

socket.addEventListener('open', function () {
console.log(`[event open] url=${url}`);
});

socket.addEventListener('message', function (event) {
feed_data(jmuxer, new Uint8Array(event.data));
});

socket.addEventListener('close', function () {
console.log(`[event close]`);
});

close_ws = () => {
clearInterval(timer_id);
socket.close(1000);
console.info("[close_ws] closed");
}
}

/**
*
* @param jmuxer
* @param event_data
*/
function feed_data(jmuxer, event_data) {
const type_flag = event_data[0];
const media_data = event_data.subarray(1);
// console.log(`[feed_data] type=${type_flag}, len=${media_data.length}`);
jmuxer.feed(type_flag ? {audio: media_data} : {video: media_data});
}

function forward_latest_frame(video) {
if (video && video.buffered && video.buffered.end(0)) {
let latest = video.buffered.end(0);
if (latest - video.currentTime > 0.3) {
video.currentTime = latest;
console.log(`[forward_latest_frame] latest=${latest}`);
}
}
}


</script>
</html>

0 comments on commit 3acbedc

Please sign in to comment.