Add VideoStreamManager for keeping track of active streams
The stream manager should help prevent zombie processes and can later be used for stopping video streams if the user exits the video before finishing for example.
This commit is contained in:
@@ -1,9 +1,9 @@
|
|||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use actix_web::{dev, Error, FromRequest, http::header, HttpRequest};
|
|
||||||
use actix_web::error::ErrorUnauthorized;
|
use actix_web::error::ErrorUnauthorized;
|
||||||
|
use actix_web::{dev, http::header, Error, FromRequest, HttpRequest};
|
||||||
use futures::future::{err, ok, Ready};
|
use futures::future::{err, ok, Ready};
|
||||||
use jsonwebtoken::{Algorithm, decode, DecodingKey, Validation};
|
use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
|
|||||||
65
src/main.rs
65
src/main.rs
@@ -2,21 +2,24 @@
|
|||||||
extern crate diesel;
|
extern crate diesel;
|
||||||
extern crate rayon;
|
extern crate rayon;
|
||||||
|
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::prelude::*;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::sync::mpsc::channel;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
use actix_files::NamedFile;
|
use actix_files::NamedFile;
|
||||||
use actix_multipart as mp;
|
use actix_multipart as mp;
|
||||||
use actix_web::web::{HttpRequest, HttpResponse, Json};
|
use actix_web::web::{HttpRequest, HttpResponse, Json};
|
||||||
use actix_web::{get, post, web, App, HttpServer, Responder};
|
use actix_web::{get, post, web, App, HttpServer, Responder};
|
||||||
use chrono::{Duration, Utc};
|
use chrono::{Duration, Utc};
|
||||||
use data::{AddFavoriteRequest, LoginRequest, ThumbnailRequest};
|
|
||||||
use futures::stream::StreamExt;
|
use futures::stream::StreamExt;
|
||||||
use jsonwebtoken::{encode, EncodingKey, Header};
|
use jsonwebtoken::{encode, EncodingKey, Header};
|
||||||
use notify::{watcher, DebouncedEvent, RecursiveMode, Watcher};
|
use notify::{watcher, DebouncedEvent, RecursiveMode, Watcher};
|
||||||
use rayon::prelude::*;
|
use rayon::prelude::*;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::fs::File;
|
|
||||||
use std::io::prelude::*;
|
use data::{AddFavoriteRequest, LoginRequest, ThumbnailRequest};
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
use std::sync::mpsc::channel;
|
|
||||||
|
|
||||||
use crate::data::{secret_key, Claims, CreateAccountRequest, Token};
|
use crate::data::{secret_key, Claims, CreateAccountRequest, Token};
|
||||||
use crate::database::{add_favorite, create_user, get_favorites, get_user, user_exists};
|
use crate::database::{add_favorite, create_user, get_favorites, get_user, user_exists};
|
||||||
@@ -171,14 +174,24 @@ async fn upload_image(_: Claims, mut payload: mp::Multipart) -> impl Responder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[post("/video/generate")]
|
#[post("/video/generate")]
|
||||||
async fn generate_video(_claims: Claims, body: web::Json<ThumbnailRequest>) -> impl Responder {
|
async fn generate_video(
|
||||||
|
_claims: Claims,
|
||||||
|
data: web::Data<AppState>,
|
||||||
|
body: web::Json<ThumbnailRequest>,
|
||||||
|
) -> impl Responder {
|
||||||
let filename = PathBuf::from(&body.path);
|
let filename = PathBuf::from(&body.path);
|
||||||
|
|
||||||
if let Some(name) = filename.file_stem() {
|
if let Some(name) = filename.file_stem() {
|
||||||
let filename = name.to_str().expect("Filename should convert to string");
|
let filename = name.to_str().expect("Filename should convert to string");
|
||||||
let playlist = format!("tmp/{}.m3u8", filename);
|
let playlist = format!("tmp/{}.m3u8", filename);
|
||||||
if let Some(path) = is_valid_path(&body.path) {
|
if let Some(path) = is_valid_path(&body.path) {
|
||||||
create_playlist(&path.to_str().unwrap(), &playlist);
|
if let Ok(mut stream) = data.stream_manager.lock() {
|
||||||
|
if let Ok(child) = create_playlist(&path.to_str().unwrap(), &playlist) {
|
||||||
|
stream.track_stream(&playlist, child)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let _ = create_playlist(&path.to_str().unwrap(), &playlist);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return HttpResponse::BadRequest().finish();
|
return HttpResponse::BadRequest().finish();
|
||||||
}
|
}
|
||||||
@@ -305,7 +318,9 @@ async fn create_thumbnails() {
|
|||||||
async fn main() -> std::io::Result<()> {
|
async fn main() -> std::io::Result<()> {
|
||||||
create_thumbnails().await;
|
create_thumbnails().await;
|
||||||
|
|
||||||
tokio::spawn(async {
|
let stream_manager = Arc::new(Mutex::new(VideoStreamManager::new()));
|
||||||
|
let stream_manager_copy = stream_manager.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
let (wtx, wrx) = channel();
|
let (wtx, wrx) = channel();
|
||||||
let mut watcher = watcher(wtx, std::time::Duration::from_secs(10)).unwrap();
|
let mut watcher = watcher(wtx, std::time::Duration::from_secs(10)).unwrap();
|
||||||
watcher
|
watcher
|
||||||
@@ -313,24 +328,25 @@ async fn main() -> std::io::Result<()> {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let ev = wrx.recv_timeout(std::time::Duration::from_secs(5));
|
if let Ok(mut manager) = stream_manager_copy.lock() {
|
||||||
match ev {
|
manager.check_for_finished_streams();
|
||||||
Ok(event) => {
|
}
|
||||||
match event {
|
|
||||||
DebouncedEvent::Create(_) => create_thumbnails().await,
|
let ev = wrx.recv_timeout(std::time::Duration::from_secs(10));
|
||||||
DebouncedEvent::Rename(_, _) => create_thumbnails().await,
|
if let Ok(event) = ev {
|
||||||
_ => continue,
|
match event {
|
||||||
};
|
DebouncedEvent::Create(_) => create_thumbnails().await,
|
||||||
}
|
DebouncedEvent::Rename(_, _) => create_thumbnails().await,
|
||||||
Err(e) => {
|
_ => continue,
|
||||||
println!("Event: {:?}", e);
|
};
|
||||||
// break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
HttpServer::new(|| {
|
let app_data = web::Data::new(AppState {
|
||||||
|
stream_manager: stream_manager.clone(),
|
||||||
|
});
|
||||||
|
HttpServer::new(move || {
|
||||||
App::new()
|
App::new()
|
||||||
.service(login)
|
.service(login)
|
||||||
.service(list_photos)
|
.service(list_photos)
|
||||||
@@ -341,9 +357,14 @@ async fn main() -> std::io::Result<()> {
|
|||||||
.service(get_video_part)
|
.service(get_video_part)
|
||||||
.service(favorites)
|
.service(favorites)
|
||||||
.service(post_add_favorite)
|
.service(post_add_favorite)
|
||||||
|
.app_data(app_data.clone())
|
||||||
})
|
})
|
||||||
.bind(dotenv::var("BIND_URL").unwrap())?
|
.bind(dotenv::var("BIND_URL").unwrap())?
|
||||||
.bind("localhost:8088")?
|
.bind("localhost:8088")?
|
||||||
.run()
|
.run()
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct AppState {
|
||||||
|
stream_manager: Arc<Mutex<VideoStreamManager>>,
|
||||||
|
}
|
||||||
|
|||||||
44
src/video.rs
44
src/video.rs
@@ -1,13 +1,43 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use std::io::Result;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::process::Command;
|
use std::process::{Child, Command, Stdio};
|
||||||
|
|
||||||
// ffmpeg -i test.mp4 -c:v h264 -flags +cgop -g 30 -hls_time 3 out.m3u8
|
// ffmpeg -i test.mp4 -c:v h264 -flags +cgop -g 30 -hls_time 3 out.m3u8
|
||||||
// ffmpeg -i "filename.mp4" -preset veryfast -c:v libx264 -f hls -hls_list_size 100 -hls_time 2 -crf 24 -vf scale=1080:-2,setsar=1:1 attempt/vid_out.m3u8
|
// ffmpeg -i "filename.mp4" -preset veryfast -c:v libx264 -f hls -hls_list_size 100 -hls_time 2 -crf 24 -vf scale=1080:-2,setsar=1:1 attempt/vid_out.m3u8
|
||||||
|
|
||||||
pub fn create_playlist(video_path: &str, playlist_file: &str) {
|
pub(crate) struct VideoStreamManager {
|
||||||
|
streams: HashMap<String, Child>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VideoStreamManager {
|
||||||
|
pub(crate) fn new() -> VideoStreamManager {
|
||||||
|
VideoStreamManager {
|
||||||
|
streams: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn track_stream(&mut self, playlist_path: &dyn ToString, child_process: Child) {
|
||||||
|
println!("Tracking process for: {:?}", playlist_path.to_string());
|
||||||
|
self.streams
|
||||||
|
.insert(playlist_path.to_string(), child_process);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn check_for_finished_streams(&mut self) {
|
||||||
|
self.streams.retain(|playlist_path, process| {
|
||||||
|
let is_done = process.try_wait().map_or(false, |status| status.is_some());
|
||||||
|
if is_done {
|
||||||
|
println!("Removing process: {:?}", playlist_path)
|
||||||
|
}
|
||||||
|
!is_done
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_playlist(video_path: &str, playlist_file: &str) -> Result<Child> {
|
||||||
if Path::new(playlist_file).exists() {
|
if Path::new(playlist_file).exists() {
|
||||||
println!("Playlist already exists: {}", playlist_file);
|
println!("Playlist already exists: {}", playlist_file);
|
||||||
return;
|
return Err(std::io::Error::from(std::io::ErrorKind::AlreadyExists));
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = Command::new("ffmpeg")
|
let result = Command::new("ffmpeg")
|
||||||
@@ -26,16 +56,22 @@ pub fn create_playlist(video_path: &str, playlist_file: &str) {
|
|||||||
.arg("-vf")
|
.arg("-vf")
|
||||||
.arg("scale=1080:-2,setsar=1:1")
|
.arg("scale=1080:-2,setsar=1:1")
|
||||||
.arg(playlist_file)
|
.arg(playlist_file)
|
||||||
|
.stdout(Stdio::null())
|
||||||
|
.stderr(Stdio::null())
|
||||||
.spawn();
|
.spawn();
|
||||||
|
|
||||||
let start_time = std::time::Instant::now();
|
let start_time = std::time::Instant::now();
|
||||||
loop {
|
loop {
|
||||||
std::thread::sleep(std::time::Duration::from_secs(1));
|
std::thread::sleep(std::time::Duration::from_secs(1));
|
||||||
|
|
||||||
if Path::new(playlist_file).exists() || std::time::Instant::now() - start_time > std::time::Duration::from_secs(5) {
|
if Path::new(playlist_file).exists()
|
||||||
|
|| std::time::Instant::now() - start_time > std::time::Duration::from_secs(5)
|
||||||
|
{
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn generate_video_thumbnail(path: &Path, destination: &Path) {
|
pub fn generate_video_thumbnail(path: &Path, destination: &Path) {
|
||||||
|
|||||||
Reference in New Issue
Block a user