Skip to content

Commit f4d4fe3

Browse files
committed
first commit
1 parent 9580ac3 commit f4d4fe3

14 files changed

+508
-42
lines changed

src-tauri/Cargo.lock

+240-22
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src-tauri/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ log = "0.4.22"
4141
rusqlite_migration = "1.3.1"
4242
base64 = "0.22.1"
4343
tauri-plugin-notification = "2.2.0"
44+
warp = "0.3.7"
4445
[target.'cfg(any(target_os = "linux", target_os = "macos"))'.dependencies]
4546
shell-words = "1.1.0"
4647
[target.'cfg(target_os = "windows")'.dependencies]

src-tauri/src/epg.rs

+16-7
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use std::{
22
sync::{
33
atomic::{AtomicBool, Ordering::Relaxed},
4-
Arc, Mutex,
4+
Arc,
55
},
66
thread::{self, sleep},
77
time::Duration,
@@ -11,6 +11,7 @@ use anyhow::{Context, Result};
1111
use chrono::Local;
1212
use tauri::{AppHandle, State};
1313
use tauri_plugin_notification::NotificationExt;
14+
use tokio::sync::Mutex;
1415

1516
use crate::{
1617
log, sql,
@@ -57,8 +58,12 @@ fn is_timestamp_over(timestamp: i64) -> Result<bool> {
5758
Ok(current_time >= time)
5859
}
5960

60-
pub fn add_epg(state: State<'_, Mutex<AppState>>, app: AppHandle, epg: EPGNotify) -> Result<()> {
61-
let mut state = state.lock().unwrap();
61+
pub async fn add_epg(
62+
state: State<'_, Mutex<AppState>>,
63+
app: AppHandle,
64+
epg: EPGNotify,
65+
) -> Result<()> {
66+
let mut state = state.lock().await;
6267
if state.thread_handle.is_some() {
6368
state.notify_stop.store(true, Relaxed);
6469
let _ = state
@@ -78,8 +83,12 @@ pub fn add_epg(state: State<'_, Mutex<AppState>>, app: AppHandle, epg: EPGNotify
7883
Ok(())
7984
}
8085

81-
pub fn remove_epg(state: State<'_, Mutex<AppState>>, app: AppHandle, epg_id: String) -> Result<()> {
82-
let mut state = state.lock().unwrap();
86+
pub async fn remove_epg(
87+
state: State<'_, Mutex<AppState>>,
88+
app: AppHandle,
89+
epg_id: String,
90+
) -> Result<()> {
91+
let mut state = state.lock().await;
8392
if state.thread_handle.is_some() {
8493
state.notify_stop.store(true, Relaxed);
8594
let _ = state
@@ -102,13 +111,13 @@ pub fn remove_epg(state: State<'_, Mutex<AppState>>, app: AppHandle, epg_id: Str
102111
Ok(())
103112
}
104113

105-
pub fn on_start_check_epg(state: State<'_, Mutex<AppState>>, app: AppHandle) -> Result<()> {
114+
pub async fn on_start_check_epg(state: State<'_, Mutex<AppState>>, app: AppHandle) -> Result<()> {
106115
sql::clean_epgs()?;
107116
let list = sql::get_epgs()?;
108117
if list.len() == 0 {
109118
return Ok(());
110119
}
111-
let mut state = state.lock().unwrap();
120+
let mut state = state.lock().await;
112121
state.notify_stop.store(false, Relaxed);
113122
let stop = state.notify_stop.clone();
114123
state

src-tauri/src/lib.rs

+38-13
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
use std::sync::Mutex;
2-
31
use anyhow::Error;
42
use tauri::{
53
menu::{Menu, MenuItem},
64
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
75
AppHandle, Manager, State,
86
};
7+
use tokio::sync::Mutex;
98
use types::{
109
AppState, Channel, CustomChannel, CustomChannelExtraData, EPGNotify, Filters, Group, IdName,
1110
Settings, Source, EPG,
@@ -16,6 +15,7 @@ pub mod log;
1615
pub mod m3u;
1716
pub mod media_type;
1817
pub mod mpv;
18+
pub mod restream;
1919
pub mod settings;
2020
pub mod share;
2121
pub mod source_type;
@@ -80,7 +80,9 @@ pub fn run() {
8080
add_epg,
8181
remove_epg,
8282
get_epg_ids,
83-
on_start_check_epg
83+
on_start_check_epg,
84+
start_restream,
85+
stop_restream
8486
])
8587
.setup(|app| {
8688
app.manage(Mutex::new(AppState {
@@ -95,14 +97,14 @@ pub fn run() {
9597
.on_menu_event(|app, event| match event.id.as_ref() {
9698
"quit" => {
9799
app.exit(0);
98-
},
100+
}
99101
"show" => {
100102
if let Some(window) = app.get_webview_window("main") {
101103
let _ = window.unminimize();
102104
let _ = window.show();
103105
let _ = window.set_focus();
104106
}
105-
},
107+
}
106108
_ => {}
107109
})
108110
.on_tray_icon_event(|tray, event| match event {
@@ -362,30 +364,53 @@ async fn download(app: AppHandle, channel: Channel) -> Result<(), String> {
362364
.map_err(map_err_frontend)
363365
}
364366

365-
#[tauri::command(async)]
366-
fn add_epg(
367+
#[tauri::command]
368+
async fn add_epg(
367369
state: State<'_, Mutex<AppState>>,
368370
app: AppHandle,
369371
epg: EPGNotify,
370372
) -> Result<(), String> {
371-
epg::add_epg(state, app, epg).map_err(map_err_frontend)
373+
epg::add_epg(state, app, epg)
374+
.await
375+
.map_err(map_err_frontend)
372376
}
373377

374378
#[tauri::command(async)]
375-
fn remove_epg(
379+
async fn remove_epg(
376380
state: State<'_, Mutex<AppState>>,
377381
app: AppHandle,
378382
epg_id: String,
379383
) -> Result<(), String> {
380-
epg::remove_epg(state, app, epg_id).map_err(map_err_frontend)
384+
epg::remove_epg(state, app, epg_id)
385+
.await
386+
.map_err(map_err_frontend)
381387
}
382388

383389
#[tauri::command(async)]
384390
fn get_epg_ids() -> Result<Vec<String>, String> {
385391
sql::get_epg_ids().map_err(map_err_frontend)
386392
}
387393

388-
#[tauri::command(async)]
389-
fn on_start_check_epg(state: State<'_, Mutex<AppState>>, app: AppHandle) -> Result<(), String> {
390-
epg::on_start_check_epg(state, app).map_err(map_err_frontend)
394+
#[tauri::command]
395+
async fn on_start_check_epg(
396+
state: State<'_, Mutex<AppState>>,
397+
app: AppHandle,
398+
) -> Result<(), String> {
399+
epg::on_start_check_epg(state, app)
400+
.await
401+
.map_err(map_err_frontend)
402+
}
403+
404+
#[tauri::command]
405+
async fn start_restream(state: State<'_, Mutex<AppState>>, channel: Channel) -> Result<(), String> {
406+
crate::restream::start_restream(state, channel)
407+
.await
408+
.map_err(map_err_frontend)
409+
}
410+
411+
#[tauri::command]
412+
async fn stop_restream(state: State<'_, Mutex<AppState>>) -> Result<(), String> {
413+
crate::restream::stop_restream(state)
414+
.await
415+
.map_err(map_err_frontend)
391416
}

src-tauri/src/restream.rs

+116
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
use std::{
2+
path::PathBuf,
3+
process::{Child, Command},
4+
};
5+
6+
use anyhow::{Context, Result};
7+
use tauri::State;
8+
use tokio::sync::{
9+
oneshot::{self, Sender},
10+
Mutex,
11+
};
12+
13+
use crate::{
14+
log::log,
15+
sql,
16+
types::{AppState, Channel},
17+
};
18+
19+
fn start_ffmpeg_listening(channel: Channel) -> Result<Child> {
20+
let headers = sql::get_channel_headers_by_id(channel.id.context("no channel id")?)?;
21+
let mut playlist_dir = get_playlist_dir()?;
22+
let mut command = Command::new("ffmpeg");
23+
command
24+
.arg("-i")
25+
.arg(channel.url.context("no channel url")?);
26+
if let Some(headers) = headers {
27+
if let Some(referrer) = headers.referrer {
28+
command.arg("-headers");
29+
command.arg(format!("Referer: {referrer}"));
30+
}
31+
if let Some(user_agent) = headers.user_agent {
32+
command.arg("-headers");
33+
command.arg(format!("User-Agent: {user_agent}"));
34+
}
35+
if let Some(origin) = headers.http_origin {
36+
command.arg("-headers");
37+
command.arg(format!("Origin: {origin}"));
38+
}
39+
if let Some(ignore_ssl) = headers.ignore_ssl {
40+
if ignore_ssl {
41+
command.arg("-tls_verify");
42+
command.arg("0");
43+
}
44+
}
45+
}
46+
let child = command
47+
.arg("-c")
48+
.arg("copy")
49+
.arg("-f")
50+
.arg("hls")
51+
.arg("-hls_time")
52+
.arg("5")
53+
.arg("-hls_list_size")
54+
.arg("6")
55+
.arg("-hls_flags")
56+
.arg("delete_segments")
57+
.arg(playlist_dir)
58+
.spawn()?;
59+
Ok(child)
60+
}
61+
62+
async fn start_web_server() -> Result<(Sender<bool>, tokio::task::JoinHandle<()>)> {
63+
let files_dir = get_restream_folder()?;
64+
let file_server = warp::fs::dir(files_dir);
65+
let (tx, rx) = oneshot::channel::<bool>();
66+
let (_, server) =
67+
warp::serve(file_server).bind_with_graceful_shutdown(([0, 0, 0, 0], 3000), async {
68+
rx.await.ok();
69+
});
70+
let handle = tokio::spawn(server);
71+
return Ok((tx, handle));
72+
}
73+
74+
pub async fn start_restream(state: State<'_, Mutex<AppState>>, channel: Channel) -> Result<()> {
75+
let mut state = state.lock().await;
76+
state.ffmpeg_child = Some(start_ffmpeg_listening(channel)?);
77+
(state.web_server_tx, state.web_server_handle) = start_web_server()
78+
.await
79+
.map(|(tx, handle)| (Some(tx), Some(handle)))?;
80+
Ok(())
81+
}
82+
83+
pub async fn stop_restream(state: State<'_, Mutex<AppState>>) -> Result<()> {
84+
let mut state = state.lock().await;
85+
let mut ffmpeg_child = state.ffmpeg_child.take().context("no ffmpeg child")?;
86+
let web_server_tx = state.web_server_tx.take().context("no web server tx")?;
87+
let web_server_handle = state
88+
.web_server_handle
89+
.take()
90+
.context("no web server handle")?;
91+
let _ = ffmpeg_child.kill();
92+
let _ = web_server_tx.send(true);
93+
let _ = ffmpeg_child.wait();
94+
let _ = web_server_handle
95+
.await
96+
.unwrap_or_else(|e| log(format!("{:?}", e)));
97+
Ok(())
98+
}
99+
100+
fn get_playlist_dir() -> Result<String> {
101+
let mut restream_folder = get_restream_folder()?;
102+
restream_folder.push("stream.m3u8");
103+
Ok(restream_folder.to_string_lossy().to_string())
104+
}
105+
106+
fn get_restream_folder() -> Result<PathBuf> {
107+
let mut path = directories::ProjectDirs::from("dev", "fredol", "open-tv")
108+
.context("can't find project folder")?
109+
.cache_dir()
110+
.to_owned();
111+
path.push("restream");
112+
if !path.exists() {
113+
std::fs::create_dir_all(&path).unwrap();
114+
}
115+
Ok(path)
116+
}

src-tauri/src/types.rs

+3
Original file line numberDiff line numberDiff line change
@@ -140,4 +140,7 @@ pub struct EPGNotify {
140140
pub struct AppState {
141141
pub notify_stop: Arc<AtomicBool>,
142142
pub thread_handle: Option<JoinHandle<Result<(), anyhow::Error>>>,
143+
pub ffmpeg_child: Option<std::process::Child>,
144+
pub web_server_tx: Option<tokio::sync::oneshot::Sender<bool>>,
145+
pub web_server_handle: Option<tokio::task::JoinHandle<()>>,
143146
}

src-tauri/stream.m3u8

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#EXTM3U
2+
#EXT-X-VERSION:3
3+
#EXT-X-TARGETDURATION:5
4+
#EXT-X-MEDIA-SEQUENCE:14
5+
#EXTINF:5.005000,
6+
stream14.ts
7+
#EXTINF:5.005000,
8+
stream15.ts
9+
#EXTINF:5.005000,
10+
stream16.ts
11+
#EXTINF:5.005000,
12+
stream17.ts
13+
#EXTINF:5.005000,
14+
stream18.ts
15+
#EXTINF:5.005000,
16+
stream19.ts

src/app/app.module.ts

+2
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { ImportModalComponent } from './import-modal/import-modal.component';
2727
import { ConfirmDeleteModalComponent } from './confirm-delete-modal/confirm-delete-modal.component';
2828
import { EpgModalComponent } from './epg-modal/epg-modal.component';
2929
import { EpgModalItemComponent } from './epg-modal/epg-modal-item/epg-modal-item.component';
30+
import { RestreamModalComponent } from './restream-modal/restream-modal.component';
3031

3132
@NgModule({
3233
declarations: [
@@ -49,6 +50,7 @@ import { EpgModalItemComponent } from './epg-modal/epg-modal-item/epg-modal-item
4950
ConfirmDeleteModalComponent,
5051
EpgModalComponent,
5152
EpgModalItemComponent,
53+
RestreamModalComponent,
5254
],
5355
imports: [
5456
BrowserModule,

src/app/channel-tile/channel-tile.component.html

+3
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@
4545
<button [hidden]="!showEPG()" mat-menu-item (click)="showEPGModal()">EPG</button>
4646
<button [hidden]="!isCustom()" mat-menu-item (click)="edit()">Edit</button>
4747
<button [hidden]="!isCustom()" mat-menu-item (click)="share()">Share</button>
48+
<button [hidden]="!isLivestream()" mat-menu-item (click)="openRestreamModal()">
49+
Re-stream
50+
</button>
4851
<button [hidden]="!isCustom()" mat-menu-item (click)="delete()">Delete</button>
4952
</ng-template>
5053
</mat-menu>

src/app/channel-tile/channel-tile.component.ts

+11
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { SourceType } from "../models/sourceType";
2222
import { EpgModalComponent } from "../epg-modal/epg-modal.component";
2323
import { EPG } from "../models/epg";
2424
import { listen, UnlistenFn } from "@tauri-apps/api/event";
25+
import { RestreamModalComponent } from "../restream-modal/restream-modal.component";
2526

2627
@Component({
2728
selector: "app-channel-tile",
@@ -284,6 +285,16 @@ export class ChannelTileComponent implements OnDestroy, AfterViewInit {
284285
this.memory.ModalRef.componentInstance.group = { ...this.channel };
285286
}
286287

288+
openRestreamModal() {
289+
this.memory.ModalRef = this.modal.open(RestreamModalComponent, {
290+
backdrop: "static",
291+
size: "xl",
292+
keyboard: false,
293+
});
294+
this.memory.ModalRef.componentInstance.channel = this.channel;
295+
this.memory.ModalRef.result.then((_) => (this.memory.ModalRef = undefined));
296+
}
297+
287298
async deleteChannel() {
288299
await this.memory.tryIPC("Successfully deleted channel", "Failed to delete channel", () =>
289300
invoke("delete_custom_channel", { id: this.channel?.id }),

src/app/restream-modal/restream-modal.component.css

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<div class="modal-header">
2+
<h4 class="modal-title lbl">Re-streaming...</h4>
3+
</div>
4+
<div class="modal-body">
5+
<p class="notice">Your re-stream server is active</p>
6+
</div>
7+
<div class="modal-footer">
8+
<button (click)="start()" class="btn btn-primary">Start</button>
9+
<button (click)="stop()" class="btn btn-danger">Stop</button>
10+
</div>

0 commit comments

Comments
 (0)