diff --git a/backend/tasks/media.py b/backend/tasks/media.py index e917975e..7446c839 100644 --- a/backend/tasks/media.py +++ b/backend/tasks/media.py @@ -26,6 +26,8 @@ IMAGE_EXTENSIONS = { ".webp", } VIDEO_EXTENSIONS = { + ".3gp", + ".avi", ".m4v", ".mkv", ".mov", @@ -97,19 +99,17 @@ def _render_video_thumbnail(attachment: TaskAttachment) -> bytes: output_path = output_file.name filter_value = "scale=640:280:force_original_aspect_ratio=increase,crop=640:280" - command = [ - ffmpeg, - "-hide_banner", - "-loglevel", - "error", - "-ss", - "0.5", - "-i", - input_path, + command_prefix = [ffmpeg, "-hide_banner", "-loglevel", "error"] + command_suffix = [ + "-map", + "0:v:0", "-frames:v", "1", "-vf", filter_value, + "-an", + "-sn", + "-dn", "-c:v", "libwebp", "-quality", @@ -117,22 +117,33 @@ def _render_video_thumbnail(attachment: TaskAttachment) -> bytes: "-y", output_path, ] - result = subprocess.run( - command, - capture_output=True, - check=False, - timeout=30, - ) - if result.returncode != 0: - command[5] = "0" - result = subprocess.run( - command, - capture_output=True, - check=False, - timeout=30, - ) - if result.returncode != 0: - raise RuntimeError(result.stderr.decode("utf-8", errors="replace")) + commands = [ + # Fast input seek works for ordinary MP4/WebM files. + [*command_prefix, "-ss", "0.5", "-i", input_path, *command_suffix], + # Accurate output seek handles fragmented MP4/MOV screen recordings + # whose first keyframe cannot be reached by input seeking. + [*command_prefix, "-i", input_path, "-ss", "0.5", *command_suffix], + # Some recordings start on a non-zero timeline. In that case use the + # first frame FFmpeg can decode instead of requiring a timestamp. + [*command_prefix, "-i", input_path, *command_suffix], + ] + last_error = "FFmpeg did not produce a video thumbnail." + for command in commands: + try: + result = subprocess.run( + command, + capture_output=True, + check=False, + timeout=30, + ) + except subprocess.TimeoutExpired as error: + last_error = str(error) + continue + if result.returncode == 0 and Path(output_path).stat().st_size > 0: + break + last_error = result.stderr.decode("utf-8", errors="replace") + else: + raise RuntimeError(last_error) return Path(output_path).read_bytes() finally: if temporary_input and input_path: diff --git a/frontend/src/components/tasks/TaskBoardNavigator.tsx b/frontend/src/components/tasks/TaskBoardNavigator.tsx index 3cac9c8c..9ce7d7f1 100644 --- a/frontend/src/components/tasks/TaskBoardNavigator.tsx +++ b/frontend/src/components/tasks/TaskBoardNavigator.tsx @@ -97,13 +97,6 @@ function boardGroupPosition( return board.group_positions?.[String(groupId)]; } -function boardGroupSortKey( - board: TaskBoardSummary, - groupId: number, -): string | undefined { - return board.group_sort_keys?.[String(groupId)]; -} - function compareBoardNames(first: TaskBoardSummary, second: TaskBoardSummary): number { return first.name.localeCompare(second.name, "ru") || first.id - second.id; } @@ -519,14 +512,18 @@ export default function TaskBoardNavigator({ (effectiveBoardSort === "priority" || effectiveBoardSort === "priority-desc") && typeof effectiveGroupFilter === "number" ) { - const firstRank = boardGroupSortKey(first, effectiveGroupFilter); - const secondRank = boardGroupSortKey(second, effectiveGroupFilter); - if (firstRank && secondRank && firstRank !== secondRank) { + const firstPosition = boardGroupPosition(first, effectiveGroupFilter); + const secondPosition = boardGroupPosition(second, effectiveGroupFilter); + if ( + firstPosition !== undefined + && secondPosition !== undefined + && firstPosition !== secondPosition + ) { const direction = effectiveBoardSort === "priority-desc" ? -1 : 1; - return (firstRank < secondRank ? -1 : 1) * direction; + return (firstPosition - secondPosition) * direction; } - if (firstRank && !secondRank) return -1; - if (!firstRank && secondRank) return 1; + if (firstPosition !== undefined && secondPosition === undefined) return -1; + if (firstPosition === undefined && secondPosition !== undefined) return 1; } if (effectiveBoardSort === "name-desc") { diff --git a/frontend/src/components/tasks/TaskCommentAttachment.tsx b/frontend/src/components/tasks/TaskCommentAttachment.tsx index e6f17fbd..76d6cdb2 100644 --- a/frontend/src/components/tasks/TaskCommentAttachment.tsx +++ b/frontend/src/components/tasks/TaskCommentAttachment.tsx @@ -98,7 +98,7 @@ export default function TaskCommentAttachment({