diff --git a/app/client/package.json b/app/client/package.json index b55ed73e..ba99922f 100644 --- a/app/client/package.json +++ b/app/client/package.json @@ -1,6 +1,6 @@ { "name": "fireshare", - "version": "1.6.10", + "version": "1.6.11", "private": true, "dependencies": { "@emotion/react": "^11.9.0", diff --git a/app/client/src/common/utils.js b/app/client/src/common/utils.js index 71aa8506..b896f5f1 100644 --- a/app/client/src/common/utils.js +++ b/app/client/src/common/utils.js @@ -206,7 +206,7 @@ export const getImageUrl = (imageId) => { * @param {string} extension - Video file extension (e.g., '.mp4', '.mkv') * @returns {Array} Array of video sources for Video.js */ -export const getVideoSources = (videoId, videoInfo, extension) => { +export const getVideoSources = (videoId, videoInfo, extension, { forceOriginal = false } = {}) => { const sources = [] const URL = getUrl() const SERVED_BY = getServedBy() @@ -216,12 +216,15 @@ export const getVideoSources = (videoId, videoInfo, extension) => { const has1080p = videoInfo?.has_1080p const hasCrop = videoInfo?.has_crop - // When a cropped version exists, point "Source" at the cropped file instead of the original - const sourceUrl = hasCrop - ? SERVED_BY === 'nginx' - ? `${URL}/_content/derived/${videoId}/${videoId}-cropped.mp4` - : `${URL}/api/video?id=${videoId}&quality=cropped` - : getVideoUrl(videoId, 'original', extension) + // forceOriginal bypasses the crop — used by the editor so admins see the full uncut video + const sourceUrl = + forceOriginal && SERVED_BY === 'nginx' + ? `${URL}/_content/video-raw/${videoId}${extension}` + : hasCrop + ? SERVED_BY === 'nginx' + ? `${URL}/_content/derived/${videoId}/${videoId}-cropped.mp4` + : `${URL}/api/video?id=${videoId}&quality=cropped` + : getVideoUrl(videoId, 'original', extension) sources.push({ src: sourceUrl, diff --git a/app/client/src/components/modal/VideoModal.js b/app/client/src/components/modal/VideoModal.js index 62a0102e..058cda37 100644 --- a/app/client/src/components/modal/VideoModal.js +++ b/app/client/src/components/modal/VideoModal.js @@ -778,8 +778,9 @@ const VideoModal = ({ key={`${vid.video_id}-${playerVersion}`} sources={getVideoSources( vid.video_id, - editMode ? { ...vid?.info, has_crop: false } : vid?.info, + vid?.info, vid.extension, + { forceOriginal: editMode }, )} poster={getPosterUrl()} autoplay={autoplay} diff --git a/app/client/src/views/GameVideos.js b/app/client/src/views/GameVideos.js index 08332ba8..7eb70460 100644 --- a/app/client/src/views/GameVideos.js +++ b/app/client/src/views/GameVideos.js @@ -460,6 +460,7 @@ const GameVideos = ({ cardSize, authenticated, searchText }) => { feedView={false} authenticated={authenticated} updateCallback={handleVideoUpdate} + onSuggestionSelect={(id) => setVideoModal({ open: true, id })} onNext={() => { const videoItems = sortedMedia.filter((m) => m.type === 'video').map((m) => m.item) const i = videoItems.findIndex((v) => v.video_id === videoModal.id) diff --git a/app/nginx/prod.conf b/app/nginx/prod.conf index bb3fd08c..54cc7b28 100644 --- a/app/nginx/prod.conf +++ b/app/nginx/prod.conf @@ -99,6 +99,14 @@ http { proxy_set_header Cookie $http_cookie; } + location = /internal/video-auth-admin { + internal; + proxy_pass http://127.0.0.1:5000/api/video/nginx-auth-admin; + proxy_pass_request_body off; + proxy_set_header Content-Length ""; + proxy_set_header Cookie $http_cookie; + } + location ~ ^/_content/derived/([^/]+)/thumbnail$ { set $video_id $1; root /processed/; @@ -113,9 +121,11 @@ http { add_header Cache-Control "public, max-age=31536000, immutable"; } - location /_content/video/ { + location ~ ^/_content/video/([\w-]+)(\.[^/]+)$ { auth_request /internal/video-auth; + sendfile off; + mp4; mp4_buffer_size 2m; mp4_max_buffer_size 50m; @@ -129,11 +139,38 @@ http { limit_rate_after 5m; - rewrite ^/_content/video/(.*)$ /$1 break; - root /processed/video_links/; + set $video_id $1; + set $video_ext $2; + root /processed/; + try_files /derived/$video_id/$video_id-cropped.mp4 /video_links/$video_id$video_ext =404; + } + + location ~ ^/_content/video-raw/([\w-]+)(\.[^/]+)$ { + auth_request /internal/video-auth-admin; + + sendfile off; + + mp4; + mp4_buffer_size 2m; + mp4_max_buffer_size 50m; + + directio 4m; + directio_alignment 512; + output_buffers 2 2m; + + add_header Cache-Control "no-store"; + add_header Accept-Ranges bytes; + + limit_rate_after 5m; + + set $video_id $1; + set $video_ext $2; + root /processed/; + try_files /video_links/$video_id$video_ext =404; } location /_content/image/ { + sendfile off; add_header Cache-Control "public, max-age=86400"; rewrite ^/_content/image/(.*)$ /$1 break; root /processed/image_links/; diff --git a/app/server/fireshare/api/video.py b/app/server/fireshare/api/video.py index e0a1e4d7..6008cee6 100644 --- a/app/server/fireshare/api/video.py +++ b/app/server/fireshare/api/video.py @@ -787,6 +787,14 @@ def unlock_video(video_id): return jsonify({"error": "Incorrect password"}), 403 +@api.route('/api/video/nginx-auth-admin') +def nginx_video_auth_admin(): + """Internal endpoint called by nginx auth_request to gate the raw (uncropped) video path.""" + if current_user.is_authenticated: + return '', 200 + return '', 403 + + @api.route('/api/video/nginx-auth') def nginx_video_auth(): """Internal endpoint called by nginx auth_request to gate password-protected video files."""