Skip to content

Commit 3acbedc

Browse files
committed
feat: web video player
1 parent 96cd930 commit 3acbedc

5 files changed

Lines changed: 181 additions & 6 deletions

File tree

README.md

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@ FLAGS:
1111
-V, --version Prints version information
1212
1313
OPTIONS:
14-
--http-flv-port <http-flv-port> disable if port is 0 [default: 0]
15-
--rtmp-port <rtmp-port> [default: 1935]
16-
--ws-fmp4-port <ws-fmp4-port> disable if port is 0 [default: 0]
17-
--ws-h264-port <ws-h264-port> disable if port is 0 [default: 0]
14+
--http-flv-port <http-flv-port> disabled if port is 0 [default: 0]
15+
--http-player-port <http-player-port> disabled if port is 0 [default: 0]
16+
--rtmp-port <rtmp-port> [default: 1935]
17+
--ws-fmp4-port <ws-fmp4-port> disabled if port is 0 [default: 0]
18+
--ws-h264-port <ws-h264-port> disabled if port is 0 [default: 0]
1819
```
1920
## Push
2021

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

3637
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.
3738

39+
**Example:**
40+
41+
1. Run `River`
42+
```shell
43+
cargo run -- --http-player-port=8080 --ws-h264-port=18000
44+
```
45+
46+
2. Push with OBS, x264, tune=zerolatency, CBR, preset=veryfast, profile=baseline
47+
48+
3. Open your browser http://localhost:8080
49+
3850
## Completed
3951
- [x] support custom width and height
4052
- [x] support audio
@@ -43,11 +55,11 @@ If you are using x264 encoding to push the stream, it is recommended that profil
4355
- [x] deal with the problem of websocket message backlog
4456
- [x] configurable startup parameters (monitoring server port)
4557
- [x] optional output formats based on the startup parameters
58+
- [x] web video player with `JMuxer` (ws-h264-port required)
4659

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

5264
## FAQ
5365

src/http_player.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
use smol::io::{AsyncWriteExt, AsyncReadExt};
2+
use smol::net::{TcpListener, TcpStream};
3+
use smol::stream::StreamExt;
4+
5+
use crate::util::spawn_and_log_error;
6+
7+
pub async fn run_server(addr: String, player_html: String) -> anyhow::Result<()> {
8+
// Open up a TCP connection and create a URL.
9+
let listener = TcpListener::bind(addr).await?;
10+
let addr = format!("http://{}", listener.local_addr()?);
11+
log::info!("HTTP-Player Server is listening to {}", addr);
12+
13+
// For each incoming TCP connection, spawn a task and call `accept`.
14+
let mut incoming = listener.incoming();
15+
while let Some(stream) = incoming.next().await {
16+
let stream = stream?;
17+
spawn_and_log_error(accept(stream, player_html.clone()));
18+
}
19+
Ok(())
20+
}
21+
22+
async fn accept(mut stream: TcpStream, player_html: String) -> anyhow::Result<()> {
23+
log::info!("[HTTP] new connection from {}", stream.peer_addr()?);
24+
25+
let mut buffer = [0; 1024];
26+
stream.read(&mut buffer).await?;
27+
28+
let header = format!("HTTP/1.1 200 OK\r\n\
29+
Content-Type: text/html;charset=UTF-8\r\n\
30+
Connection: close\r\n\
31+
Content-Length: {}\r\n\
32+
Cache-Control: no-cache\r\n\
33+
Access-Control-Allow-Origin: *\r\n\
34+
\r\n\
35+
{}", player_html.len(), player_html);
36+
stream.write_all(header.as_bytes()).await?;
37+
stream.flush().await?;
38+
Ok(())
39+
}

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ extern crate num_derive;
33

44
mod eventbus;
55
pub mod http_flv;
6+
pub mod http_player;
67
pub mod protocol;
78
pub mod rtmp_server;
89
pub mod util;

src/main.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
use clap::crate_version;
22
use clap::Clap;
3-
use river::{ws_h264, ws_fmp4, util, http_flv};
3+
use river::{ws_h264, ws_fmp4, util, http_flv, http_player};
44
use river::rtmp_server::accept_loop;
55
use river::util::spawn_and_log_error;
66

77

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

30+
let player_html = include_str!("../static/player.html");
31+
let player_html = player_html.replace("{/*$INJECTED_CONTEXT*/}", &format!("{{port: {}}}", opts.ws_h264_port));
32+
33+
if opts.http_player_port > 0 {
34+
spawn_and_log_error(http_player::run_server(format!("0.0.0.0:{}", opts.http_player_port), player_html));
35+
}
2836
if opts.http_flv_port > 0 {
2937
spawn_and_log_error(http_flv::run_server(format!("0.0.0.0:{}", opts.http_flv_port)));
3038
}

static/player.html

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
<!doctype html>
2+
<html lang="zh">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport"
6+
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
7+
<meta http-equiv="X-UA-Compatible" content="ie=edge">
8+
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/semantic-ui@2.4.2/dist/semantic.min.css">
9+
<script src="https://cdn.jsdelivr.net/npm/jquery@3.2.1/dist/jquery.min.js"></script>
10+
<script src="https://cdn.jsdelivr.net/npm/semantic-ui@2.4.2/dist/semantic.min.js"></script>
11+
<script src="https://cdn.jsdelivr.net/npm/jmuxer@2.0.2/dist/jmuxer.min.js"></script>
12+
<title>River Player</title>
13+
</head>
14+
<body>
15+
<div id="container" style="margin: 10px auto 0; width: 1024px;">
16+
<video style="border: 1px solid #333; width: 1024px;" autoplay id="player"></video>
17+
<br>
18+
<div>
19+
<button class="ui labeled icon button" onclick="open_ws(url, jmuxer);">
20+
<i class="play icon"></i>
21+
Play
22+
</button>
23+
24+
<button class="ui labeled icon button" onclick="close_ws()">
25+
<i class="stop icon"></i>
26+
Stop
27+
</button>
28+
29+
<button class="ui labeled icon disabled button" onclick="">
30+
<i class="expand icon"></i>
31+
Expand
32+
</button>
33+
</div>
34+
</div>
35+
</body>
36+
<script>
37+
const ctx = {/*$INJECTED_CONTEXT*/};
38+
39+
const url = `ws://${document.domain}:${ctx.port}/websocket${window.location.pathname}`;
40+
let jmuxer = null;
41+
let timer_id = null;
42+
43+
let close_ws = () => {
44+
};
45+
46+
$(function main() {
47+
jmuxer = new JMuxer({
48+
flushingTime: 100,
49+
fps: 30,
50+
node: 'player',
51+
mode: 'both', /* available values are: both, audio and video */
52+
debug: false
53+
});
54+
55+
let player = video_element = document.getElementById('player');
56+
57+
document.addEventListener("visibilitychange", function () {
58+
forward_latest_frame(player);
59+
});
60+
61+
timer_id = setInterval(() => {
62+
forward_latest_frame(player);
63+
}, 2000);
64+
});
65+
66+
function open_ws(url, jmuxer) {
67+
close_ws();
68+
69+
const socket = new WebSocket(url);
70+
socket.binaryType = 'arraybuffer';
71+
72+
socket.addEventListener('open', function () {
73+
console.log(`[event open] url=${url}`);
74+
});
75+
76+
socket.addEventListener('message', function (event) {
77+
feed_data(jmuxer, new Uint8Array(event.data));
78+
});
79+
80+
socket.addEventListener('close', function () {
81+
console.log(`[event close]`);
82+
});
83+
84+
close_ws = () => {
85+
clearInterval(timer_id);
86+
socket.close(1000);
87+
console.info("[close_ws] closed");
88+
}
89+
}
90+
91+
/**
92+
*
93+
* @param jmuxer
94+
* @param event_data
95+
*/
96+
function feed_data(jmuxer, event_data) {
97+
const type_flag = event_data[0];
98+
const media_data = event_data.subarray(1);
99+
// console.log(`[feed_data] type=${type_flag}, len=${media_data.length}`);
100+
jmuxer.feed(type_flag ? {audio: media_data} : {video: media_data});
101+
}
102+
103+
function forward_latest_frame(video) {
104+
if (video && video.buffered && video.buffered.end(0)) {
105+
let latest = video.buffered.end(0);
106+
if (latest - video.currentTime > 0.3) {
107+
video.currentTime = latest;
108+
console.log(`[forward_latest_frame] latest=${latest}`);
109+
}
110+
}
111+
}
112+
113+
114+
</script>
115+
</html>

0 commit comments

Comments
 (0)