diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/media_files/upload/upload_studio_shot/extract_frames_from_zip.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/media_files/upload/upload_studio_shot/extract_frames_from_zip.rs index a43ff9496c..14964d976e 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/media_files/upload/upload_studio_shot/extract_frames_from_zip.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/media_files/upload/upload_studio_shot/extract_frames_from_zip.rs @@ -2,7 +2,7 @@ use std::collections::{HashMap, HashSet}; use std::io::{BufReader, Cursor, Read}; use std::path::{Path, PathBuf}; -use log::{error, info, warn}; +use log::{debug, error, info, warn}; use once_cell::sync::Lazy; use zip::ZipArchive; @@ -48,10 +48,10 @@ pub fn extract_frames_from_zip>( let entries = get_relevant_zip_entries(&mut archive)?; for entry in entries.iter() { - info!("Entry: {:?}", entry); + debug!("Entry: {:?}", entry); if !entry.is_frame { - info!("Skipping entry (not a frame): {:?}", entry); + debug!("Skipping entry (not a frame): {:?}", entry); continue; } @@ -74,6 +74,8 @@ pub fn extract_frames_from_zip>( let output_path = frame_temp_dir.as_ref().join(filesystem_name); + info!("Creating: {:?}", output_path); + let mut output_file = std::fs::File::create(&output_path) .map_err(|err| { error!("Problem creating file {:?} : {:?}", output_path, err); @@ -103,7 +105,7 @@ fn get_relevant_zip_entries(archive: &mut ZipArchive>>) let mut entries = Vec::new(); for i in 0..(archive.len()) { - info!("Reading file {}...", i); + debug!("Reading file {}...", i); let mut file = archive.by_index(i) .map_err(|err| { @@ -114,8 +116,8 @@ fn get_relevant_zip_entries(archive: &mut ZipArchive>>) let filename = file.name(); let filename_lowercase = filename.to_lowercase(); - info!("File {} is {:?} - is file = {}", i, filename, file.is_file()); - info!("Enclosed name: {:?}", file.enclosed_name()); + debug!("File {} is {:?} - is file = {}", i, filename, file.is_file()); + debug!("Enclosed name: {:?}", file.enclosed_name()); if file.is_dir() { continue; @@ -147,7 +149,7 @@ fn get_relevant_zip_entries(archive: &mut ZipArchive>>) } for entry in entries.iter() { - info!("Entry: {:?}", entry); + debug!("Entry: {:?}", entry); } Ok(entries) diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/media_files/upload/upload_studio_shot/ffmpeg_frames_to_mp4.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/media_files/upload/upload_studio_shot/ffmpeg_frames_to_mp4.rs new file mode 100644 index 0000000000..f90dfa3d31 --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/media_files/upload/upload_studio_shot/ffmpeg_frames_to_mp4.rs @@ -0,0 +1,66 @@ +use std::io::Write; +use std::path::{Path, PathBuf}; +use std::process::Command; + +use anyhow::bail; +use log::{error, info}; + +use errors::AnyhowResult; +use filesys::path_to_string::path_to_string; +use videos::ffprobe_get_info::{ffprobe_get_info, VideoDimensions, VideoDuration}; + +#[derive(Copy,Clone,Debug)] +pub enum FrameType { + Png, + Jpg, +} + +pub struct OutputFile { + pub path: PathBuf, + pub dimensions: Option, + pub duration: Option, +} + +pub fn ffmpeg_frames_to_mp4(frame_input_directory: &Path, frame_type: FrameType, frame_rate: u16) -> AnyhowResult { + let glob_pattern = match frame_type { + FrameType::Png => format!("{}/*.png", path_to_string(frame_input_directory)), + FrameType::Jpg => format!("{}/*.jpg", path_to_string(frame_input_directory)), + }; + + let output_file_path = frame_input_directory.join("output.mp4"); + + info!("Calling ffmpeg to convert frames to mp4..."); + + // ffmpeg -framerate 30 -pattern_type glob -i '*.png' \ + // -c:v libx264 -pix_fmt yuv420p out.mp4 + let output = Command::new("ffmpeg") + .arg("-framerate") + .arg(format!("{}", frame_rate)) + .arg("-pattern_type") + .arg("glob") + .arg("-i") + .arg(glob_pattern) + .arg("-c:v") + .arg("libx264") + .arg("-pix_fmt") + .arg("yuv420p") + .arg(&output_file_path) + .output()?; + + if !output.status.success() { + error!("bad exit status: {}", output.status); + + let _r = std::io::stdout().write_all(&output.stdout); + let _r = std::io::stderr().write_all(&output.stderr); + + bail!("ffmpeg failed: {:?}", output.status.to_string()); + } + + let info = ffprobe_get_info(&output_file_path)?; + + Ok(OutputFile { + path: output_file_path, + duration: info.duration, + dimensions: info.dimensions, + }) +} diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/media_files/upload/upload_studio_shot/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/media_files/upload/upload_studio_shot/mod.rs index 4514bdbda7..170fdf246e 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/media_files/upload/upload_studio_shot/mod.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/media_files/upload/upload_studio_shot/mod.rs @@ -1,2 +1,3 @@ pub mod extract_frames_from_zip; -pub mod upload_studio_shot_media_file_handler; \ No newline at end of file +pub mod upload_studio_shot_media_file_handler; +mod ffmpeg_frames_to_mp4; \ No newline at end of file diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/media_files/upload/upload_studio_shot/upload_studio_shot_media_file_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/media_files/upload/upload_studio_shot/upload_studio_shot_media_file_handler.rs index 7b5d8d4fb6..ee0ab232f2 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/media_files/upload/upload_studio_shot/upload_studio_shot_media_file_handler.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/media_files/upload/upload_studio_shot/upload_studio_shot_media_file_handler.rs @@ -20,6 +20,7 @@ use enums::by_table::media_files::media_file_engine_category::MediaFileEngineCat use enums::by_table::media_files::media_file_type::MediaFileType; use enums::common::visibility::Visibility; use hashing::sha256::sha256_hash_bytes::sha256_hash_bytes; +use hashing::sha256::sha256_hash_file::sha256_hash_file; use http_server_common::request::get_request_ip::get_request_ip; use mimetypes::mimetype_for_bytes::get_mimetype_for_bytes; use mimetypes::mimetype_to_extension::mimetype_to_extension; @@ -34,6 +35,7 @@ use crate::http_server::endpoints::media_files::upload::upload_error::MediaFileU use crate::http_server::endpoints::media_files::upload::upload_new_scene_media_file_handler::UploadNewSceneMediaFileForm; use crate::http_server::endpoints::media_files::upload::upload_pmx::extract_and_upload_pmx_files::{extract_and_upload_pmx_files, PmxError}; use crate::http_server::endpoints::media_files::upload::upload_studio_shot::extract_frames_from_zip::{extract_frames_from_zip, ExtractFramesError}; +use crate::http_server::endpoints::media_files::upload::upload_studio_shot::ffmpeg_frames_to_mp4::{ffmpeg_frames_to_mp4, FrameType}; use crate::http_server::web_utils::user_session::require_moderator::{require_moderator, RequireModeratorError, UseDatabase}; use crate::state::server_state::ServerState; use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format; @@ -216,7 +218,7 @@ pub async fn upload_studio_shot_media_file_handler( MediaFileUploadError::ServerError })?; - extract_frames_from_zip(&file_bytes, frame_temp_dir) + extract_frames_from_zip(&file_bytes, frame_temp_dir.path()) .map_err(|err| { warn!("Extract frames error: {:?}", err); match err { @@ -232,46 +234,67 @@ pub async fn upload_studio_shot_media_file_handler( // ==================== FFMPEG ==================== // + let video_file_details = ffmpeg_frames_to_mp4(frame_temp_dir.path(), FrameType::Png, 30) + .map_err(|err| { + warn!("FFMPEG error: {:?}", err); + MediaFileUploadError::ServerError + })?; + let hash = sha256_hash_file(&video_file_details.path) + .map_err(|io_error| { + error!("Problem hashing bytes: {:?}", io_error); + MediaFileUploadError::ServerError + })?; - // ==================== UPLOAD AND SAVE ==================== // + // ==================== UPLOAD ==================== // - // TODO DELETE ME - let token = MediaFileToken::new("test_token".to_string()); + const PREFIX : Option<&str> = Some("upload_"); + const SUFFIX : Option<&str> = Some(".mp4"); -// const PREFIX : Option<&str> = Some("upload_"); -// const SUFFIX : Option<&str> = Some(".pmx"); -// -// // TODO(bt, 2024-02-22): This should be a transaction. -// let (token, record_id) = insert_media_file_from_file_upload(InsertMediaFileFromUploadArgs { -// maybe_media_class: Some(MediaFileClass::Dimensional), -// media_file_type: MediaFileType::Pmx, -// maybe_creator_user_token: Some(&user_session.get_strongly_typed_user_token()), -// maybe_creator_anonymous_visitor_token: maybe_avt_token.as_ref(), -// creator_ip_address: &ip_address, -// creator_set_visibility, -// upload_type: UploadType::Filesystem, -// maybe_engine_category: Some(engine_category), -// maybe_animation_type, -// maybe_mime_type: Some("application/octet-stream"), -// file_size_bytes: pmx_details.file_size_bytes, -// maybe_duration_millis, -// sha256_checksum: &pmx_details.sha256_checksum, -// maybe_scene_source_media_file_token: None, -// is_intermediate_system_file: false, -// maybe_title: maybe_title.as_deref(), -// public_bucket_directory_hash: pmx_details.pmx_public_upload_path.get_object_hash(), -// maybe_public_bucket_prefix: PREFIX, -// maybe_public_bucket_extension: SUFFIX, -// pool: &server_state.mysql_pool, -// }) -// .await -// .map_err(|err| { -// warn!("New file creation DB error: {:?}", err); -// MediaFileUploadError::ServerError -// })?; -// -// info!("new media file id: {} token: {:?}", record_id, &token); + let bucket_path = MediaFileBucketPath::generate_new(PREFIX, SUFFIX); + + server_state.public_bucket_client.upload_filename_with_content_type( + bucket_path.get_full_object_path_str(), + &video_file_details.path, + "video/mp4") + .await + .map_err(|e| { + error!("Upload video to bucket error: {:?}", e); + MediaFileUploadError::ServerError + })?; + + // ==================== SAVE RECORD ==================== // + + // TODO(bt, 2024-02-22): This should be a transaction. + let (token, record_id) = insert_media_file_from_file_upload(InsertMediaFileFromUploadArgs { + maybe_media_class: Some(MediaFileClass::Video), + media_file_type: MediaFileType::Mp4, + maybe_creator_user_token: Some(&user_session.user_token_typed), + maybe_creator_anonymous_visitor_token: maybe_avt_token.as_ref(), + creator_ip_address: &ip_address, + creator_set_visibility, + upload_type: UploadType::StorytellerEngine, + maybe_engine_category: None, + maybe_animation_type: None, + maybe_mime_type: Some("video/mp4"), + file_size_bytes: 0, // TODO + maybe_duration_millis: video_file_details.duration.map(|duration| duration.millis as u64), + sha256_checksum: &hash, + maybe_scene_source_media_file_token: None, + is_intermediate_system_file: false, // TODO + maybe_title: maybe_title.as_deref(), + public_bucket_directory_hash: bucket_path.get_object_hash(), + maybe_public_bucket_prefix: PREFIX, + maybe_public_bucket_extension: SUFFIX, + pool: &server_state.mysql_pool, + }) + .await + .map_err(|err| { + warn!("New file creation DB error: {:?}", err); + MediaFileUploadError::ServerError + })?; + + info!("new media file id: {} token: {:?}", record_id, &token); Ok(Json(UploadStudioShotSuccessResponse { success: true,