
Recherche avancée
Médias (91)
-
Collections - Formulaire de création rapide
19 février 2013, par
Mis à jour : Février 2013
Langue : français
Type : Image
-
Les Miserables
4 juin 2012, par
Mis à jour : Février 2013
Langue : English
Type : Texte
-
Ne pas afficher certaines informations : page d’accueil
23 novembre 2011, par
Mis à jour : Novembre 2011
Langue : français
Type : Image
-
The Great Big Beautiful Tomorrow
28 octobre 2011, par
Mis à jour : Octobre 2011
Langue : English
Type : Texte
-
Richard Stallman et la révolution du logiciel libre - Une biographie autorisée (version epub)
28 octobre 2011, par
Mis à jour : Octobre 2011
Langue : English
Type : Texte
-
Rennes Emotion Map 2010-11
19 octobre 2011, par
Mis à jour : Juillet 2013
Langue : français
Type : Texte
Autres articles (77)
-
Personnaliser en ajoutant son logo, sa bannière ou son image de fond
5 septembre 2013, parCertains thèmes prennent en compte trois éléments de personnalisation : l’ajout d’un logo ; l’ajout d’une bannière l’ajout d’une image de fond ;
-
Ecrire une actualité
21 juin 2013, parPrésentez les changements dans votre MédiaSPIP ou les actualités de vos projets sur votre MédiaSPIP grâce à la rubrique actualités.
Dans le thème par défaut spipeo de MédiaSPIP, les actualités sont affichées en bas de la page principale sous les éditoriaux.
Vous pouvez personnaliser le formulaire de création d’une actualité.
Formulaire de création d’une actualité Dans le cas d’un document de type actualité, les champs proposés par défaut sont : Date de publication ( personnaliser la date de publication ) (...) -
Publier sur MédiaSpip
13 juin 2013Puis-je poster des contenus à partir d’une tablette Ipad ?
Oui, si votre Médiaspip installé est à la version 0.2 ou supérieure. Contacter au besoin l’administrateur de votre MédiaSpip pour le savoir
Sur d’autres sites (6968)
-
My stitched frames colors looks very different from my original video, causing my video to not be able to stitch it back properly [closed]
27 mai 2024, par Wer WerI am trying to extract some frames off my video to do some form of steganography. I accidentally used a 120fps video, causing the files to be too big when i extract every single frame. To fix this, I decided to calculate how many frames is needed to hide the bits (LSB replacement for every 8 bit) and then extract only certain amount of frames. This means


- 

- if i only need 1 frame, ill extract frame0.png
- ill remove frame0 from the original video
- encode my data into frame0.png
- stitch frame0 back into ffv1 video
- concatenate frame0 video to the rest of the video, frame0 video in front.












I can do extraction and remove frame0 from the video. However, when looking at frame0.mkv and the original.mkv, i realised the colors seemed to be different.
Frame0.mkv
original.mkv


This causes a glitch during the stitching of videos together, where the end of the video has some corrupted pixels. Not only that, it stops the video at where frame0 ends. I think those corrupted pixels were supposed to be original.mkv pixels, but they did not concatenate properly.
results.mkv


I use an ffmpeg sub command to extract frames and stitch them


def split_into_frames(self, ffv1_video, hidden_text_length):
 if not ffv1_video.endswith(".mkv"):
 ffv1_video += ".mkv"

 ffv1_video_path = os.path.join(self.here, ffv1_video)
 ffv1_video = cv2.VideoCapture(ffv1_video_path)

 currentframe = 0
 total_frame_bits = 0
 frames_to_remove = []

 while True:
 ret, frame = ffv1_video.read()
 if ret:
 name = os.path.join(self.here, "data", f"frame{currentframe}.png")
 print("Creating..." + name)
 cv2.imwrite(name, frame)

 current_frame_path = os.path.join(
 self.here, "data", f"frame{currentframe}.png"
 )

 if os.path.exists(current_frame_path):
 binary_data = self.read_frame_binary(current_frame_path)

 if (total_frame_bits // 8) >= hidden_text_length:
 print("Complete")
 break
 total_frame_bits += len(binary_data)
 frames_to_remove.append(currentframe)
 currentframe += 1
 else:
 print("Complete")
 break

 ffv1_video.release()

 # Remove the extracted frames from the original video
 self.remove_frames_from_video(ffv1_video_path, frames_to_remove)




This code splits the video into the required number of frames. It checks if the total amount of frame bits is enough to encode the hidden text


def remove_frames_from_video(self, input_video, frames_to_remove):
 if not input_video.endswith(".mkv"):
 input_video += ".mkv"

 input_video_path = os.path.join(self.here, input_video)

 # Create a filter string to exclude specific frames
 filter_str = (
 "select='not("
 + "+".join([f"eq(n\,{frame})" for frame in frames_to_remove])
 + ")',setpts=N/FRAME_RATE/TB"
 )

 # Temporary output video path
 output_video_path = os.path.join(self.here, "temp_output.mkv")

 command = [
 "ffmpeg",
 "-y",
 "-i",
 input_video_path,
 "-vf",
 filter_str,
 "-c:v",
 "ffv1",
 "-level",
 "3",
 "-coder",
 "1",
 "-context",
 "1",
 "-g",
 "1",
 "-slices",
 "4",
 "-slicecrc",
 "1",
 "-an", # Remove audio
 output_video_path,
 ]

 try:
 subprocess.run(command, check=True)
 print(f"Frames removed. Temporary video created at {output_video_path}")

 # Replace the original video with the new video
 os.replace(output_video_path, input_video_path)
 print(f"Original video replaced with updated video at {input_video_path}")

 # Re-add the trimmed audio to the new video
 self.trim_audio_and_add_to_video(input_video_path, frames_to_remove)
 except subprocess.CalledProcessError as e:
 print(f"An error occurred: {e}")
 if os.path.exists(output_video_path):
 os.remove(output_video_path)

def trim_audio_and_add_to_video(self, video_path, frames_to_remove):
 # Calculate the new duration based on the remaining frames
 fps = 60 # Assuming the framerate is 60 fps
 total_frames_removed = len(frames_to_remove)
 original_duration = self.get_video_duration(video_path)
 new_duration = original_duration - (total_frames_removed / fps)

 # Extract and trim the audio
 audio_path = os.path.join(self.here, "trimmed_audio.aac")
 command_extract_trim = [
 "ffmpeg",
 "-y",
 "-i",
 video_path,
 "-t",
 str(new_duration),
 "-q:a",
 "0",
 "-map",
 "a",
 audio_path,
 ]
 try:
 subprocess.run(command_extract_trim, check=True)
 print(f"Audio successfully trimmed and extracted to {audio_path}")

 # Add the trimmed audio back to the video
 final_video_path = video_path.replace(".mkv", "_final.mkv")
 command_add_audio = [
 "ffmpeg",
 "-y",
 "-i",
 video_path,
 "-i",
 audio_path,
 "-c:v",
 "copy",
 "-c:a",
 "aac",
 "-strict",
 "experimental",
 final_video_path,
 ]
 subprocess.run(command_add_audio, check=True)
 print(f"Final video with trimmed audio created at {final_video_path}")

 # Replace the original video with the final video
 os.replace(final_video_path, video_path)
 print(f"Original video replaced with final video at {video_path}")
 except subprocess.CalledProcessError as e:
 print(f"An error occurred: {e}")

def get_video_duration(self, video_path):
 command = [
 "ffprobe",
 "-v",
 "error",
 "-show_entries",
 "format=duration",
 "-of",
 "default=noprint_wrappers=1:nokey=1",
 video_path,
 ]
 try:
 result = subprocess.run(
 command, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE
 )
 duration = float(result.stdout.decode().strip())
 return duration
 except subprocess.CalledProcessError as e:
 print(f"An error occurred while getting video duration: {e}")
 return 0.0



here ill remove all the frames that has been extracted from the video


def stitch_frames_to_video(self, ffv1_video, framerate=60):
 # this command is another ffmpeg subcommand.
 # it takes every single frame from data1 directory and stitch it back into a ffv1 video
 if not ffv1_video.endswith(".mkv"):
 ffv1_video += ".mkv"

 output_video_path = os.path.join(self.here, ffv1_video)

 command = [
 "ffmpeg",
 "-y",
 "-framerate",
 str(framerate),
 "-i",
 os.path.join(self.frames_directory, "frame%d.png"),
 "-c:v",
 "ffv1",
 "-level",
 "3",
 "-coder",
 "1",
 "-context",
 "1",
 "-g",
 "1",
 "-slices",
 "4",
 "-slicecrc",
 "1",
 output_video_path,
 ]

 try:
 subprocess.run(command, check=True)
 print(f"Video successfully created at {output_video_path}")
 except subprocess.CalledProcessError as e:
 print(f"An error occurred: {e}")



after encoding the frames, ill try to stitch the frames back into ffv1 video


def concatenate_videos(self, video1_path, video2_path, output_path):
 if not video1_path.endswith(".mkv"):
 video1_path += ".mkv"
 if not video2_path.endswith(".mkv"):
 video2_path += ".mkv"
 if not output_path.endswith(".mkv"):
 output_path += ".mkv"

 video1_path = os.path.join(self.here, video1_path)
 video2_path = os.path.join(self.here, video2_path)
 output_video_path = os.path.join(self.here, output_path)

 # Create a text file with the paths of the videos to concatenate
 concat_list_path = os.path.join(self.here, "concat_list.txt")
 with open(concat_list_path, "w") as f:
 f.write(f"file '{video1_path}'\n")
 f.write(f"file '{video2_path}'\n")

 command = [
 "ffmpeg",
 "-y",
 "-f",
 "concat",
 "-safe",
 "0",
 "-i",
 concat_list_path,
 "-c",
 "copy",
 output_video_path,
 ]

 try:
 subprocess.run(command, check=True)
 print(f"Videos successfully concatenated into {output_video_path}")
 os.remove(concat_list_path) # Clean up the temporary file
 except subprocess.CalledProcessError as e:
 print(f"An error occurred: {e}")



now i try to concatenate the frames video with the original video, but it is corrupting as the colors are different.


this code does the other processing by removing all the extracted frames from the video, as well as trimming the audio (but i think ill be removing the audio trimming as i realised it is not needed at all)


I think its because .png frames will lose colors when they get extracted out. The only work around I know is to extract every single frame. But this causes the program to run too long as for a 12 second video, I will extract 700++ frames. Is there a way to fix this ?


my full code


import json
import os
import shutil
import magic
import ffmpeg
import cv2
import numpy as np
import subprocess
from PIL import Image
import glob


import json
import os
import shutil
import magic
import ffmpeg
import cv2
import numpy as np
import subprocess
from PIL import Image
import glob


class FFV1Steganography:
 def __init__(self):
 self.here = os.path.dirname(os.path.abspath(__file__))

 # Create a folder to save the frames
 self.frames_directory = os.path.join(self.here, "data")
 try:
 if not os.path.exists(self.frames_directory):
 os.makedirs(self.frames_directory)
 except OSError:
 print("Error: Creating directory of data")

 def read_hidden_text(self, filename):
 file_path_txt = os.path.join(self.here, filename)
 # Read the content of the file in binary mode
 with open(file_path_txt, "rb") as f:
 hidden_text_content = f.read()
 return hidden_text_content

 def calculate_length_of_hidden_text(self, filename):
 hidden_text_content = self.read_hidden_text(filename)
 # Convert each byte to its binary representation and join them
 return len("".join(format(byte, "08b") for byte in hidden_text_content))

 def find_raw_video_file(self, filename):
 file_extensions = [".mp4", ".mkv", ".avi"]
 for ext in file_extensions:
 file_path = os.path.join(self.here, filename + ext)
 if os.path.isfile(file_path):
 return file_path
 return None

 def convert_video(self, input_file, ffv1_video):
 # this function is the same as running this command line
 # ffmpeg -i video.mp4 -t 12 -c:v ffv1 -level 3 -coder 1 -context 1 -g 1 -slices 4 -slicecrc 1 -c:a copy output.mkv

 # in order to run any ffmpeg subprocess, you have to have ffmpeg installed into the computer.
 # https://ffmpeg.org/download.html

 # WARNING:
 # the ffmpeg you should download is not the same as the ffmpeg library for python.
 # you need to download the exe from the link above, then add ffmpeg bin directory to system variables
 output_file = os.path.join(self.here, ffv1_video)

 if not output_file.endswith(".mkv"):
 output_file += ".mkv"

 command = [
 "ffmpeg",
 "-y",
 "-i",
 input_file,
 "-t",
 "12",
 "-c:v",
 "ffv1",
 "-level",
 "3",
 "-coder",
 "1",
 "-context",
 "1",
 "-g",
 "1",
 "-slices",
 "4",
 "-slicecrc",
 "1",
 "-c:a",
 "copy",
 output_file,
 ]

 try:
 subprocess.run(command, check=True)
 print(f"Conversion successful: {output_file}")
 return output_file
 except subprocess.CalledProcessError as e:
 print(f"Error during conversion: {e}")

 def extract_audio(self, ffv1_video, audio_path):
 # Ensure the audio output file has the correct extension
 if not audio_path.endswith(".aac"):
 audio_path += ".aac"

 # Full path to the extracted audio file
 extracted_audio = os.path.join(self.here, audio_path)

 if not ffv1_video.endswith(".mkv"):
 ffv1_video += ".mkv"

 command = [
 "ffmpeg",
 "-i",
 ffv1_video,
 "-q:a",
 "0",
 "-map",
 "a",
 extracted_audio,
 ]
 try:
 result = subprocess.run(
 command, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE
 )
 print(f"Audio successfully extracted to {extracted_audio}")
 print(result.stdout.decode())
 print(result.stderr.decode())
 except subprocess.CalledProcessError as e:
 print(f"An error occurred: {e}")
 print(e.stdout.decode())
 print(e.stderr.decode())

 def read_frame_binary(self, frame_path):
 # Open the image and convert to binary
 with open(frame_path, "rb") as f:
 binary_content = f.read()
 binary_string = "".join(format(byte, "08b") for byte in binary_content)
 return binary_string

 def remove_frames_from_video(self, input_video, frames_to_remove):
 if not input_video.endswith(".mkv"):
 input_video += ".mkv"

 input_video_path = os.path.join(self.here, input_video)

 # Create a filter string to exclude specific frames
 filter_str = (
 "select='not("
 + "+".join([f"eq(n\,{frame})" for frame in frames_to_remove])
 + ")',setpts=N/FRAME_RATE/TB"
 )

 # Temporary output video path
 output_video_path = os.path.join(self.here, "temp_output.mkv")

 command = [
 "ffmpeg",
 "-y",
 "-i",
 input_video_path,
 "-vf",
 filter_str,
 "-c:v",
 "ffv1",
 "-level",
 "3",
 "-coder",
 "1",
 "-context",
 "1",
 "-g",
 "1",
 "-slices",
 "4",
 "-slicecrc",
 "1",
 "-an", # Remove audio
 output_video_path,
 ]

 try:
 subprocess.run(command, check=True)
 print(f"Frames removed. Temporary video created at {output_video_path}")

 # Replace the original video with the new video
 os.replace(output_video_path, input_video_path)
 print(f"Original video replaced with updated video at {input_video_path}")

 # Re-add the trimmed audio to the new video
 self.trim_audio_and_add_to_video(input_video_path, frames_to_remove)
 except subprocess.CalledProcessError as e:
 print(f"An error occurred: {e}")
 if os.path.exists(output_video_path):
 os.remove(output_video_path)

 def trim_audio_and_add_to_video(self, video_path, frames_to_remove):
 # Calculate the new duration based on the remaining frames
 fps = 60 # Assuming the framerate is 60 fps
 total_frames_removed = len(frames_to_remove)
 original_duration = self.get_video_duration(video_path)
 new_duration = original_duration - (total_frames_removed / fps)

 # Extract and trim the audio
 audio_path = os.path.join(self.here, "trimmed_audio.aac")
 command_extract_trim = [
 "ffmpeg",
 "-y",
 "-i",
 video_path,
 "-t",
 str(new_duration),
 "-q:a",
 "0",
 "-map",
 "a",
 audio_path,
 ]
 try:
 subprocess.run(command_extract_trim, check=True)
 print(f"Audio successfully trimmed and extracted to {audio_path}")

 # Add the trimmed audio back to the video
 final_video_path = video_path.replace(".mkv", "_final.mkv")
 command_add_audio = [
 "ffmpeg",
 "-y",
 "-i",
 video_path,
 "-i",
 audio_path,
 "-c:v",
 "copy",
 "-c:a",
 "aac",
 "-strict",
 "experimental",
 final_video_path,
 ]
 subprocess.run(command_add_audio, check=True)
 print(f"Final video with trimmed audio created at {final_video_path}")

 # Replace the original video with the final video
 os.replace(final_video_path, video_path)
 print(f"Original video replaced with final video at {video_path}")
 except subprocess.CalledProcessError as e:
 print(f"An error occurred: {e}")

 def get_video_duration(self, video_path):
 command = [
 "ffprobe",
 "-v",
 "error",
 "-show_entries",
 "format=duration",
 "-of",
 "default=noprint_wrappers=1:nokey=1",
 video_path,
 ]
 try:
 result = subprocess.run(
 command, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE
 )
 duration = float(result.stdout.decode().strip())
 return duration
 except subprocess.CalledProcessError as e:
 print(f"An error occurred while getting video duration: {e}")
 return 0.0

 def split_into_frames(self, ffv1_video, hidden_text_length):
 if not ffv1_video.endswith(".mkv"):
 ffv1_video += ".mkv"

 ffv1_video_path = os.path.join(self.here, ffv1_video)
 ffv1_video = cv2.VideoCapture(ffv1_video_path)

 currentframe = 0
 total_frame_bits = 0
 frames_to_remove = []

 while True:
 ret, frame = ffv1_video.read()
 if ret:
 name = os.path.join(self.here, "data", f"frame{currentframe}.png")
 print("Creating..." + name)
 cv2.imwrite(name, frame)

 current_frame_path = os.path.join(
 self.here, "data", f"frame{currentframe}.png"
 )

 if os.path.exists(current_frame_path):
 binary_data = self.read_frame_binary(current_frame_path)

 if (total_frame_bits // 8) >= hidden_text_length:
 print("Complete")
 break
 total_frame_bits += len(binary_data)
 frames_to_remove.append(currentframe)
 currentframe += 1
 else:
 print("Complete")
 break

 ffv1_video.release()

 # Remove the extracted frames from the original video
 self.remove_frames_from_video(ffv1_video_path, frames_to_remove)

 def stitch_frames_to_video(self, ffv1_video, framerate=60):
 # this command is another ffmpeg subcommand.
 # it takes every single frame from data1 directory and stitch it back into a ffv1 video
 if not ffv1_video.endswith(".mkv"):
 ffv1_video += ".mkv"

 output_video_path = os.path.join(self.here, ffv1_video)

 command = [
 "ffmpeg",
 "-y",
 "-framerate",
 str(framerate),
 "-i",
 os.path.join(self.frames_directory, "frame%d.png"),
 "-c:v",
 "ffv1",
 "-level",
 "3",
 "-coder",
 "1",
 "-context",
 "1",
 "-g",
 "1",
 "-slices",
 "4",
 "-slicecrc",
 "1",
 output_video_path,
 ]

 try:
 subprocess.run(command, check=True)
 print(f"Video successfully created at {output_video_path}")
 except subprocess.CalledProcessError as e:
 print(f"An error occurred: {e}")

 def add_audio_to_video(self, encoded_video, audio_path, final_video):
 # the audio will be lost during splitting and restitching.
 # that is why previously we separated the audio from video and saved it as aac.
 # now, we can put the audio back into the video, again using ffmpeg subcommand.

 if not encoded_video.endswith(".mkv"):
 encoded_video += ".mkv"

 if not final_video.endswith(".mkv"):
 final_video += ".mkv"

 if not audio_path.endswith(".aac"):
 audio_path += ".aac"

 final_output_path = os.path.join(self.here, final_video)

 command = [
 "ffmpeg",
 "-y",
 "-i",
 os.path.join(self.here, encoded_video),
 "-i",
 os.path.join(self.here, audio_path),
 "-c:v",
 "copy",
 "-c:a",
 "aac",
 "-strict",
 "experimental",
 final_output_path,
 ]
 try:
 subprocess.run(command, check=True)
 print(f"Final video with audio created at {final_output_path}")
 except subprocess.CalledProcessError as e:
 print(f"An error occurred: {e}")

 def concatenate_videos(self, video1_path, video2_path, output_path):
 if not video1_path.endswith(".mkv"):
 video1_path += ".mkv"
 if not video2_path.endswith(".mkv"):
 video2_path += ".mkv"
 if not output_path.endswith(".mkv"):
 output_path += ".mkv"

 video1_path = os.path.join(self.here, video1_path)
 video2_path = os.path.join(self.here, video2_path)
 output_video_path = os.path.join(self.here, output_path)

 # Create a text file with the paths of the videos to concatenate
 concat_list_path = os.path.join(self.here, "concat_list.txt")
 with open(concat_list_path, "w") as f:
 f.write(f"file '{video1_path}'\n")
 f.write(f"file '{video2_path}'\n")

 command = [
 "ffmpeg",
 "-y",
 "-f",
 "concat",
 "-safe",
 "0",
 "-i",
 concat_list_path,
 "-c",
 "copy",
 output_video_path,
 ]

 try:
 subprocess.run(command, check=True)
 print(f"Videos successfully concatenated into {output_video_path}")
 os.remove(concat_list_path) # Clean up the temporary file
 except subprocess.CalledProcessError as e:
 print(f"An error occurred: {e}")

 def cleanup(self, files_to_delete):
 # Delete specified files
 for file in files_to_delete:
 file_path = os.path.join(self.here, file)
 if os.path.exists(file_path):
 os.remove(file_path)
 print(f"Deleted file: {file_path}")
 else:
 print(f"File not found: {file_path}")

 # Delete the frames directory and its contents
 if os.path.exists(self.frames_directory):
 shutil.rmtree(self.frames_directory)
 print(f"Deleted directory and its contents: {self.frames_directory}")
 else:
 print(f"Directory not found: {self.frames_directory}")


if __name__ == "__main__":
 stego = FFV1Steganography()

 # original video (mp4,mkv,avi)
 original_video = "video"
 # converted ffv1 video
 ffv1_video = "output"
 # extracted audio
 extracted_audio = "audio"
 # encoded video without sound
 encoded_video = "encoded"
 # final result video, encoded, with sound
 final_video = "result"

 # region --hidden text processing --
 hidden_text = stego.read_hidden_text("hiddentext.txt")
 hidden_text_length = stego.calculate_length_of_hidden_text("hiddentext.txt")
 # endregion

 # region -- raw video locating --
 raw_video_file = stego.find_raw_video_file(original_video)
 if raw_video_file:
 print(f"Found video file: {raw_video_file}")
 else:
 print("video.mp4 not found.")
 # endregion

 # region -- video processing INPUT--
 # converted_video_file = stego.convert_video(raw_video_file, ffv1_video)
 # if converted_video_file and os.path.exists(converted_video_file):
 # stego.extract_audio(converted_video_file, extracted_audio)
 # else:
 # print(f"Conversion failed: {converted_video_file} not found.")

 # stego.split_into_frames(ffv1_video, hidden_text_length * 50000)
 # endregion

 # region -- video processing RESULT --
 # stego.stitch_frames_to_video(encoded_video)
 stego.concatenate_videos(encoded_video, ffv1_video, final_video)
 # stego.add_audio_to_video(final_video, extracted_audio, final_video)
 # endregion

 # region -- cleanup --
 files_to_delete = [
 extracted_audio + ".aac",
 encoded_video + ".mkv",
 ffv1_video + ".mkv",
 ]

 stego.cleanup(files_to_delete)
 # endregion








Edit for results expectations :
I dont know if there is a way to match the exact color encoding between the stitched png frames and the rest of the ffv1 video. Is there a way I can extract the frames such that the color, encoding or anything I may not know about ffv1 match the original ffv1 video ?


-
FFMPEG My stitched frames colors looks very different from my original video, causing my video to not be able to stitch it back properly
26 mai 2024, par Wer WerI am trying to extract some frames off my video to do some form of steganography. I accidentally used a 120fps video, causing the files to be too big when i extract every single frame. To fix this, I decided to calculate how many frames is needed to hide the bits (LSB replacement for every 8 bit) and then extract only certain amount of frames. This means


- 

- if i only need 1 frame, ill extract frame0.png
- ill remove frame0 from the original video
- encode my data into frame0.png
- stitch frame0 back into ffv1 video
- concatenate frame0 video to the rest of the video, frame0 video in front.












I can do extraction and remove frame0 from the video. However, when looking at frame0.mkv and the original.mkv, i realised the colors seemed to be different.
Frame0.mkv
original.mkv


This causes a glitch during the stitching of videos together, where the end of the video has some corrupted pixels. Not only that, it stops the video at where frame0 ends. I think those corrupted pixels were supposed to be original.mkv pixels, but they did not concatenate properly.
results.mkv


I use an ffmpeg sub command to extract frames and stitch them


def split_into_frames(self, ffv1_video, hidden_text_length):
 if not ffv1_video.endswith(".mkv"):
 ffv1_video += ".mkv"

 ffv1_video_path = os.path.join(self.here, ffv1_video)
 ffv1_video = cv2.VideoCapture(ffv1_video_path)

 currentframe = 0
 total_frame_bits = 0
 frames_to_remove = []

 while True:
 ret, frame = ffv1_video.read()
 if ret:
 name = os.path.join(self.here, "data", f"frame{currentframe}.png")
 print("Creating..." + name)
 cv2.imwrite(name, frame)

 current_frame_path = os.path.join(
 self.here, "data", f"frame{currentframe}.png"
 )

 if os.path.exists(current_frame_path):
 binary_data = self.read_frame_binary(current_frame_path)

 if (total_frame_bits // 8) >= hidden_text_length:
 print("Complete")
 break
 total_frame_bits += len(binary_data)
 frames_to_remove.append(currentframe)
 currentframe += 1
 else:
 print("Complete")
 break

 ffv1_video.release()

 # Remove the extracted frames from the original video
 self.remove_frames_from_video(ffv1_video_path, frames_to_remove)




This code splits the video into the required number of frames. It checks if the total amount of frame bits is enough to encode the hidden text


def remove_frames_from_video(self, input_video, frames_to_remove):
 if not input_video.endswith(".mkv"):
 input_video += ".mkv"

 input_video_path = os.path.join(self.here, input_video)

 # Create a filter string to exclude specific frames
 filter_str = (
 "select='not("
 + "+".join([f"eq(n\,{frame})" for frame in frames_to_remove])
 + ")',setpts=N/FRAME_RATE/TB"
 )

 # Temporary output video path
 output_video_path = os.path.join(self.here, "temp_output.mkv")

 command = [
 "ffmpeg",
 "-y",
 "-i",
 input_video_path,
 "-vf",
 filter_str,
 "-c:v",
 "ffv1",
 "-level",
 "3",
 "-coder",
 "1",
 "-context",
 "1",
 "-g",
 "1",
 "-slices",
 "4",
 "-slicecrc",
 "1",
 "-an", # Remove audio
 output_video_path,
 ]

 try:
 subprocess.run(command, check=True)
 print(f"Frames removed. Temporary video created at {output_video_path}")

 # Replace the original video with the new video
 os.replace(output_video_path, input_video_path)
 print(f"Original video replaced with updated video at {input_video_path}")

 # Re-add the trimmed audio to the new video
 self.trim_audio_and_add_to_video(input_video_path, frames_to_remove)
 except subprocess.CalledProcessError as e:
 print(f"An error occurred: {e}")
 if os.path.exists(output_video_path):
 os.remove(output_video_path)

def trim_audio_and_add_to_video(self, video_path, frames_to_remove):
 # Calculate the new duration based on the remaining frames
 fps = 60 # Assuming the framerate is 60 fps
 total_frames_removed = len(frames_to_remove)
 original_duration = self.get_video_duration(video_path)
 new_duration = original_duration - (total_frames_removed / fps)

 # Extract and trim the audio
 audio_path = os.path.join(self.here, "trimmed_audio.aac")
 command_extract_trim = [
 "ffmpeg",
 "-y",
 "-i",
 video_path,
 "-t",
 str(new_duration),
 "-q:a",
 "0",
 "-map",
 "a",
 audio_path,
 ]
 try:
 subprocess.run(command_extract_trim, check=True)
 print(f"Audio successfully trimmed and extracted to {audio_path}")

 # Add the trimmed audio back to the video
 final_video_path = video_path.replace(".mkv", "_final.mkv")
 command_add_audio = [
 "ffmpeg",
 "-y",
 "-i",
 video_path,
 "-i",
 audio_path,
 "-c:v",
 "copy",
 "-c:a",
 "aac",
 "-strict",
 "experimental",
 final_video_path,
 ]
 subprocess.run(command_add_audio, check=True)
 print(f"Final video with trimmed audio created at {final_video_path}")

 # Replace the original video with the final video
 os.replace(final_video_path, video_path)
 print(f"Original video replaced with final video at {video_path}")
 except subprocess.CalledProcessError as e:
 print(f"An error occurred: {e}")

def get_video_duration(self, video_path):
 command = [
 "ffprobe",
 "-v",
 "error",
 "-show_entries",
 "format=duration",
 "-of",
 "default=noprint_wrappers=1:nokey=1",
 video_path,
 ]
 try:
 result = subprocess.run(
 command, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE
 )
 duration = float(result.stdout.decode().strip())
 return duration
 except subprocess.CalledProcessError as e:
 print(f"An error occurred while getting video duration: {e}")
 return 0.0



here ill remove all the frames that has been extracted from the video


def stitch_frames_to_video(self, ffv1_video, framerate=60):
 # this command is another ffmpeg subcommand.
 # it takes every single frame from data1 directory and stitch it back into a ffv1 video
 if not ffv1_video.endswith(".mkv"):
 ffv1_video += ".mkv"

 output_video_path = os.path.join(self.here, ffv1_video)

 command = [
 "ffmpeg",
 "-y",
 "-framerate",
 str(framerate),
 "-i",
 os.path.join(self.frames_directory, "frame%d.png"),
 "-c:v",
 "ffv1",
 "-level",
 "3",
 "-coder",
 "1",
 "-context",
 "1",
 "-g",
 "1",
 "-slices",
 "4",
 "-slicecrc",
 "1",
 output_video_path,
 ]

 try:
 subprocess.run(command, check=True)
 print(f"Video successfully created at {output_video_path}")
 except subprocess.CalledProcessError as e:
 print(f"An error occurred: {e}")



after encoding the frames, ill try to stitch the frames back into ffv1 video


def concatenate_videos(self, video1_path, video2_path, output_path):
 if not video1_path.endswith(".mkv"):
 video1_path += ".mkv"
 if not video2_path.endswith(".mkv"):
 video2_path += ".mkv"
 if not output_path.endswith(".mkv"):
 output_path += ".mkv"

 video1_path = os.path.join(self.here, video1_path)
 video2_path = os.path.join(self.here, video2_path)
 output_video_path = os.path.join(self.here, output_path)

 # Create a text file with the paths of the videos to concatenate
 concat_list_path = os.path.join(self.here, "concat_list.txt")
 with open(concat_list_path, "w") as f:
 f.write(f"file '{video1_path}'\n")
 f.write(f"file '{video2_path}'\n")

 command = [
 "ffmpeg",
 "-y",
 "-f",
 "concat",
 "-safe",
 "0",
 "-i",
 concat_list_path,
 "-c",
 "copy",
 output_video_path,
 ]

 try:
 subprocess.run(command, check=True)
 print(f"Videos successfully concatenated into {output_video_path}")
 os.remove(concat_list_path) # Clean up the temporary file
 except subprocess.CalledProcessError as e:
 print(f"An error occurred: {e}")



now i try to concatenate the frames video with the original video, but it is corrupting as the colors are different.


this code does the other processing by removing all the extracted frames from the video, as well as trimming the audio (but i think ill be removing the audio trimming as i realised it is not needed at all)


I think its because .png frames will lose colors when they get extracted out. The only work around I know is to extract every single frame. But this causes the program to run too long as for a 12 second video, I will extract 700++ frames. Is there a way to fix this ?


my full code


import json
import os
import shutil
import magic
import ffmpeg
import cv2
import numpy as np
import subprocess
from PIL import Image
import glob


import json
import os
import shutil
import magic
import ffmpeg
import cv2
import numpy as np
import subprocess
from PIL import Image
import glob


class FFV1Steganography:
 def __init__(self):
 self.here = os.path.dirname(os.path.abspath(__file__))

 # Create a folder to save the frames
 self.frames_directory = os.path.join(self.here, "data")
 try:
 if not os.path.exists(self.frames_directory):
 os.makedirs(self.frames_directory)
 except OSError:
 print("Error: Creating directory of data")

 def read_hidden_text(self, filename):
 file_path_txt = os.path.join(self.here, filename)
 # Read the content of the file in binary mode
 with open(file_path_txt, "rb") as f:
 hidden_text_content = f.read()
 return hidden_text_content

 def calculate_length_of_hidden_text(self, filename):
 hidden_text_content = self.read_hidden_text(filename)
 # Convert each byte to its binary representation and join them
 return len("".join(format(byte, "08b") for byte in hidden_text_content))

 def find_raw_video_file(self, filename):
 file_extensions = [".mp4", ".mkv", ".avi"]
 for ext in file_extensions:
 file_path = os.path.join(self.here, filename + ext)
 if os.path.isfile(file_path):
 return file_path
 return None

 def convert_video(self, input_file, ffv1_video):
 # this function is the same as running this command line
 # ffmpeg -i video.mp4 -t 12 -c:v ffv1 -level 3 -coder 1 -context 1 -g 1 -slices 4 -slicecrc 1 -c:a copy output.mkv

 # in order to run any ffmpeg subprocess, you have to have ffmpeg installed into the computer.
 # https://ffmpeg.org/download.html

 # WARNING:
 # the ffmpeg you should download is not the same as the ffmpeg library for python.
 # you need to download the exe from the link above, then add ffmpeg bin directory to system variables
 output_file = os.path.join(self.here, ffv1_video)

 if not output_file.endswith(".mkv"):
 output_file += ".mkv"

 command = [
 "ffmpeg",
 "-y",
 "-i",
 input_file,
 "-t",
 "12",
 "-c:v",
 "ffv1",
 "-level",
 "3",
 "-coder",
 "1",
 "-context",
 "1",
 "-g",
 "1",
 "-slices",
 "4",
 "-slicecrc",
 "1",
 "-c:a",
 "copy",
 output_file,
 ]

 try:
 subprocess.run(command, check=True)
 print(f"Conversion successful: {output_file}")
 return output_file
 except subprocess.CalledProcessError as e:
 print(f"Error during conversion: {e}")

 def extract_audio(self, ffv1_video, audio_path):
 # Ensure the audio output file has the correct extension
 if not audio_path.endswith(".aac"):
 audio_path += ".aac"

 # Full path to the extracted audio file
 extracted_audio = os.path.join(self.here, audio_path)

 if not ffv1_video.endswith(".mkv"):
 ffv1_video += ".mkv"

 command = [
 "ffmpeg",
 "-i",
 ffv1_video,
 "-q:a",
 "0",
 "-map",
 "a",
 extracted_audio,
 ]
 try:
 result = subprocess.run(
 command, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE
 )
 print(f"Audio successfully extracted to {extracted_audio}")
 print(result.stdout.decode())
 print(result.stderr.decode())
 except subprocess.CalledProcessError as e:
 print(f"An error occurred: {e}")
 print(e.stdout.decode())
 print(e.stderr.decode())

 def read_frame_binary(self, frame_path):
 # Open the image and convert to binary
 with open(frame_path, "rb") as f:
 binary_content = f.read()
 binary_string = "".join(format(byte, "08b") for byte in binary_content)
 return binary_string

 def remove_frames_from_video(self, input_video, frames_to_remove):
 if not input_video.endswith(".mkv"):
 input_video += ".mkv"

 input_video_path = os.path.join(self.here, input_video)

 # Create a filter string to exclude specific frames
 filter_str = (
 "select='not("
 + "+".join([f"eq(n\,{frame})" for frame in frames_to_remove])
 + ")',setpts=N/FRAME_RATE/TB"
 )

 # Temporary output video path
 output_video_path = os.path.join(self.here, "temp_output.mkv")

 command = [
 "ffmpeg",
 "-y",
 "-i",
 input_video_path,
 "-vf",
 filter_str,
 "-c:v",
 "ffv1",
 "-level",
 "3",
 "-coder",
 "1",
 "-context",
 "1",
 "-g",
 "1",
 "-slices",
 "4",
 "-slicecrc",
 "1",
 "-an", # Remove audio
 output_video_path,
 ]

 try:
 subprocess.run(command, check=True)
 print(f"Frames removed. Temporary video created at {output_video_path}")

 # Replace the original video with the new video
 os.replace(output_video_path, input_video_path)
 print(f"Original video replaced with updated video at {input_video_path}")

 # Re-add the trimmed audio to the new video
 self.trim_audio_and_add_to_video(input_video_path, frames_to_remove)
 except subprocess.CalledProcessError as e:
 print(f"An error occurred: {e}")
 if os.path.exists(output_video_path):
 os.remove(output_video_path)

 def trim_audio_and_add_to_video(self, video_path, frames_to_remove):
 # Calculate the new duration based on the remaining frames
 fps = 60 # Assuming the framerate is 60 fps
 total_frames_removed = len(frames_to_remove)
 original_duration = self.get_video_duration(video_path)
 new_duration = original_duration - (total_frames_removed / fps)

 # Extract and trim the audio
 audio_path = os.path.join(self.here, "trimmed_audio.aac")
 command_extract_trim = [
 "ffmpeg",
 "-y",
 "-i",
 video_path,
 "-t",
 str(new_duration),
 "-q:a",
 "0",
 "-map",
 "a",
 audio_path,
 ]
 try:
 subprocess.run(command_extract_trim, check=True)
 print(f"Audio successfully trimmed and extracted to {audio_path}")

 # Add the trimmed audio back to the video
 final_video_path = video_path.replace(".mkv", "_final.mkv")
 command_add_audio = [
 "ffmpeg",
 "-y",
 "-i",
 video_path,
 "-i",
 audio_path,
 "-c:v",
 "copy",
 "-c:a",
 "aac",
 "-strict",
 "experimental",
 final_video_path,
 ]
 subprocess.run(command_add_audio, check=True)
 print(f"Final video with trimmed audio created at {final_video_path}")

 # Replace the original video with the final video
 os.replace(final_video_path, video_path)
 print(f"Original video replaced with final video at {video_path}")
 except subprocess.CalledProcessError as e:
 print(f"An error occurred: {e}")

 def get_video_duration(self, video_path):
 command = [
 "ffprobe",
 "-v",
 "error",
 "-show_entries",
 "format=duration",
 "-of",
 "default=noprint_wrappers=1:nokey=1",
 video_path,
 ]
 try:
 result = subprocess.run(
 command, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE
 )
 duration = float(result.stdout.decode().strip())
 return duration
 except subprocess.CalledProcessError as e:
 print(f"An error occurred while getting video duration: {e}")
 return 0.0

 def split_into_frames(self, ffv1_video, hidden_text_length):
 if not ffv1_video.endswith(".mkv"):
 ffv1_video += ".mkv"

 ffv1_video_path = os.path.join(self.here, ffv1_video)
 ffv1_video = cv2.VideoCapture(ffv1_video_path)

 currentframe = 0
 total_frame_bits = 0
 frames_to_remove = []

 while True:
 ret, frame = ffv1_video.read()
 if ret:
 name = os.path.join(self.here, "data", f"frame{currentframe}.png")
 print("Creating..." + name)
 cv2.imwrite(name, frame)

 current_frame_path = os.path.join(
 self.here, "data", f"frame{currentframe}.png"
 )

 if os.path.exists(current_frame_path):
 binary_data = self.read_frame_binary(current_frame_path)

 if (total_frame_bits // 8) >= hidden_text_length:
 print("Complete")
 break
 total_frame_bits += len(binary_data)
 frames_to_remove.append(currentframe)
 currentframe += 1
 else:
 print("Complete")
 break

 ffv1_video.release()

 # Remove the extracted frames from the original video
 self.remove_frames_from_video(ffv1_video_path, frames_to_remove)

 def stitch_frames_to_video(self, ffv1_video, framerate=60):
 # this command is another ffmpeg subcommand.
 # it takes every single frame from data1 directory and stitch it back into a ffv1 video
 if not ffv1_video.endswith(".mkv"):
 ffv1_video += ".mkv"

 output_video_path = os.path.join(self.here, ffv1_video)

 command = [
 "ffmpeg",
 "-y",
 "-framerate",
 str(framerate),
 "-i",
 os.path.join(self.frames_directory, "frame%d.png"),
 "-c:v",
 "ffv1",
 "-level",
 "3",
 "-coder",
 "1",
 "-context",
 "1",
 "-g",
 "1",
 "-slices",
 "4",
 "-slicecrc",
 "1",
 output_video_path,
 ]

 try:
 subprocess.run(command, check=True)
 print(f"Video successfully created at {output_video_path}")
 except subprocess.CalledProcessError as e:
 print(f"An error occurred: {e}")

 def add_audio_to_video(self, encoded_video, audio_path, final_video):
 # the audio will be lost during splitting and restitching.
 # that is why previously we separated the audio from video and saved it as aac.
 # now, we can put the audio back into the video, again using ffmpeg subcommand.

 if not encoded_video.endswith(".mkv"):
 encoded_video += ".mkv"

 if not final_video.endswith(".mkv"):
 final_video += ".mkv"

 if not audio_path.endswith(".aac"):
 audio_path += ".aac"

 final_output_path = os.path.join(self.here, final_video)

 command = [
 "ffmpeg",
 "-y",
 "-i",
 os.path.join(self.here, encoded_video),
 "-i",
 os.path.join(self.here, audio_path),
 "-c:v",
 "copy",
 "-c:a",
 "aac",
 "-strict",
 "experimental",
 final_output_path,
 ]
 try:
 subprocess.run(command, check=True)
 print(f"Final video with audio created at {final_output_path}")
 except subprocess.CalledProcessError as e:
 print(f"An error occurred: {e}")

 def concatenate_videos(self, video1_path, video2_path, output_path):
 if not video1_path.endswith(".mkv"):
 video1_path += ".mkv"
 if not video2_path.endswith(".mkv"):
 video2_path += ".mkv"
 if not output_path.endswith(".mkv"):
 output_path += ".mkv"

 video1_path = os.path.join(self.here, video1_path)
 video2_path = os.path.join(self.here, video2_path)
 output_video_path = os.path.join(self.here, output_path)

 # Create a text file with the paths of the videos to concatenate
 concat_list_path = os.path.join(self.here, "concat_list.txt")
 with open(concat_list_path, "w") as f:
 f.write(f"file '{video1_path}'\n")
 f.write(f"file '{video2_path}'\n")

 command = [
 "ffmpeg",
 "-y",
 "-f",
 "concat",
 "-safe",
 "0",
 "-i",
 concat_list_path,
 "-c",
 "copy",
 output_video_path,
 ]

 try:
 subprocess.run(command, check=True)
 print(f"Videos successfully concatenated into {output_video_path}")
 os.remove(concat_list_path) # Clean up the temporary file
 except subprocess.CalledProcessError as e:
 print(f"An error occurred: {e}")

 def cleanup(self, files_to_delete):
 # Delete specified files
 for file in files_to_delete:
 file_path = os.path.join(self.here, file)
 if os.path.exists(file_path):
 os.remove(file_path)
 print(f"Deleted file: {file_path}")
 else:
 print(f"File not found: {file_path}")

 # Delete the frames directory and its contents
 if os.path.exists(self.frames_directory):
 shutil.rmtree(self.frames_directory)
 print(f"Deleted directory and its contents: {self.frames_directory}")
 else:
 print(f"Directory not found: {self.frames_directory}")


if __name__ == "__main__":
 stego = FFV1Steganography()

 # original video (mp4,mkv,avi)
 original_video = "video"
 # converted ffv1 video
 ffv1_video = "output"
 # extracted audio
 extracted_audio = "audio"
 # encoded video without sound
 encoded_video = "encoded"
 # final result video, encoded, with sound
 final_video = "result"

 # region --hidden text processing --
 hidden_text = stego.read_hidden_text("hiddentext.txt")
 hidden_text_length = stego.calculate_length_of_hidden_text("hiddentext.txt")
 # endregion

 # region -- raw video locating --
 raw_video_file = stego.find_raw_video_file(original_video)
 if raw_video_file:
 print(f"Found video file: {raw_video_file}")
 else:
 print("video.mp4 not found.")
 # endregion

 # region -- video processing INPUT--
 # converted_video_file = stego.convert_video(raw_video_file, ffv1_video)
 # if converted_video_file and os.path.exists(converted_video_file):
 # stego.extract_audio(converted_video_file, extracted_audio)
 # else:
 # print(f"Conversion failed: {converted_video_file} not found.")

 # stego.split_into_frames(ffv1_video, hidden_text_length * 50000)
 # endregion

 # region -- video processing RESULT --
 # stego.stitch_frames_to_video(encoded_video)
 stego.concatenate_videos(encoded_video, ffv1_video, final_video)
 # stego.add_audio_to_video(final_video, extracted_audio, final_video)
 # endregion

 # region -- cleanup --
 files_to_delete = [
 extracted_audio + ".aac",
 encoded_video + ".mkv",
 ffv1_video + ".mkv",
 ]

 stego.cleanup(files_to_delete)
 # endregion








-
When I use ffmpeg to go from a video to frames, and then back to video, the duration is different between the videos
24 février 2024, par bluepandaI am trying to use
ffmpeg
to convert from a .mp4 (or .mov) video into individual frames, do some processing on those frames, and then convert back to .mp4. The problem is that the resulting video I create is a different duration than the input - I can see this visually when I play the two videos side by side. The difference is not large (i.e.00:00:00.50
for the input video and00:00:00.52
for the output video), but when the videos are looped next to each other they get out of sync.

Here is information about the input video retrieved using
fluent-ffmpeg
'sffmpeg.ffprobe(videoPath)
:

metadata {
 streams: [
 {
 index: 0,
 codec_name: 'h264',
 codec_long_name: 'H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10',
 profile: 'High',
 codec_type: 'video',
 codec_tag_string: 'avc1',
 codec_tag: '0x31637661',
 width: 1080,
 height: 1920,
 coded_width: 1080,
 coded_height: 1920,
 closed_captions: 0,
 has_b_frames: 2,
 sample_aspect_ratio: 'N/A',
 display_aspect_ratio: 'N/A',
 pix_fmt: 'yuv420p',
 level: 40,
 color_range: 'tv',
 color_space: 'bt709',
 color_transfer: 'bt709',
 color_primaries: 'bt709',
 chroma_location: 'left',
 field_order: 'unknown',
 refs: 1,
 is_avc: 'true',
 nal_length_size: 4,
 id: 'N/A',
 r_frame_rate: '30000/1001',
 avg_frame_rate: '27000/1001',
 time_base: '1/30000',
 start_pts: 0,
 start_time: 0,
 duration_ts: 15100,
 duration: 0.503333,
 bit_rate: 5660223,
 max_bit_rate: 'N/A',
 bits_per_raw_sample: 8,
 nb_frames: 36,
 nb_read_frames: 'N/A',
 nb_read_packets: 'N/A',
 tags: [Object],
 disposition: [Object]
 },
 {
 index: 1,
 codec_name: 'aac',
 codec_long_name: 'AAC (Advanced Audio Coding)',
 profile: 'LC',
 codec_type: 'audio',
 codec_tag_string: 'mp4a',
 codec_tag: '0x6134706d',
 sample_fmt: 'fltp',
 sample_rate: 48000,
 channels: 2,
 channel_layout: 'stereo',
 bits_per_sample: 0,
 id: 'N/A',
 r_frame_rate: '0/0',
 avg_frame_rate: '0/0',
 time_base: '1/48000',
 start_pts: 0,
 start_time: 0,
 duration_ts: 24160,
 duration: 0.503333,
 bit_rate: 248416,
 max_bit_rate: 'N/A',
 bits_per_raw_sample: 'N/A',
 nb_frames: 27,
 nb_read_frames: 'N/A',
 nb_read_packets: 'N/A',
 tags: [Object],
 disposition: [Object]
 }
 ],
 format: {
 filename: '/Users/name/images/input.mp4',
 nb_streams: 2,
 nb_programs: 0,
 format_name: 'mov,mp4,m4a,3gp,3g2,mj2',
 format_long_name: 'QuickTime / MOV',
 start_time: 0,
 duration: 0.503333,
 size: 963879,
 bit_rate: 15319941,
 probe_score: 100,
 tags: {
 major_brand: 'mp42',
 minor_version: '1',
 compatible_brands: 'isommp41mp42',
 creation_time: '2024-02-14T01:21:12.000000Z'
 }
 },
 chapters: []
}



and here is from running
ffprobe
directly :

ffprobe '/Users/name/images/input.mp4'
ffprobe version 6.1.1 Copyright (c) 2007-2023 the FFmpeg developers
 built with Apple clang version 15.0.0 (clang-1500.1.0.2.5)
 configuration: --prefix=/opt/homebrew/Cellar/ffmpeg/6.1.1_2 --enable-shared --enable-pthreads --enable-version3 --cc=clang --host-cflags= --host-ldflags='-Wl,-ld_classic' --enable-ffplay --enable-gnutls --enable-gpl --enable-libaom --enable-libaribb24 --enable-libbluray --enable-libdav1d --enable-libharfbuzz --enable-libjxl --enable-libmp3lame --enable-libopus --enable-librav1e --enable-librist --enable-librubberband --enable-libsnappy --enable-libsrt --enable-libssh --enable-libsvtav1 --enable-libtesseract --enable-libtheora --enable-libvidstab --enable-libvmaf --enable-libvorbis --enable-libvpx --enable-libwebp --enable-libx264 --enable-libx265 --enable-libxml2 --enable-libxvid --enable-lzma --enable-libfontconfig --enable-libfreetype --enable-frei0r --enable-libass --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libopenjpeg --enable-libopenvino --enable-libspeex --enable-libsoxr --enable-libzmq --enable-libzimg --disable-libjack --disable-indev=jack --enable-videotoolbox --enable-audiotoolbox --enable-neon
 libavutil 58. 29.100 / 58. 29.100
 libavcodec 60. 31.102 / 60. 31.102
 libavformat 60. 16.100 / 60. 16.100
 libavdevice 60. 3.100 / 60. 3.100
 libavfilter 9. 12.100 / 9. 12.100
 libswscale 7. 5.100 / 7. 5.100
 libswresample 4. 12.100 / 4. 12.100
 libpostproc 57. 3.100 / 57. 3.100
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from '/Users/name/images/input.mp4':
 Metadata:
 major_brand : mp42
 minor_version : 1
 compatible_brands: isommp41mp42
 creation_time : 2024-02-14T01:21:12.000000Z
 Duration: 00:00:00.50, start: 0.000000, bitrate: 15319 kb/s
 Stream #0:0[0x1](und): Video: h264 (High) (avc1 / 0x31637661), yuv420p(tv, bt709, progressive), 1080x1920, 5660 kb/s, 26.97 fps, 29.97 tbr, 30k tbn (default)
 Metadata:
 creation_time : 2024-02-14T01:21:12.000000Z
 handler_name : Core Media Video
 vendor_id : [0][0][0][0]
 encoder : AVC Coding
 Stream #0:1[0x2](und): Audio: aac (LC) (mp4a / 0x6134706D), 48000 Hz, stereo, fltp, 248 kb/s (default)
 Metadata:
 creation_time : 2024-02-14T01:21:12.000000Z
 handler_name : Core Media Audio
 vendor_id : [0][0][0][0]



And this is my command to go from video to frames :


ffmpeg -i /Users/name/images/input.mp4 -y -f image2 /Users/name/images/frames/%d.png



After which I convert the frames back to video with this - note that I get by seeing
avg_frame_rate
is27000/1001 = 26.97302697
:

ffmpeg -r 26.973026973026972 -i /Users/name/images/frames/%d.png -y -r 26.973026973026972 -b:v 5660223k -f mp4 -pix_fmt yuv420p -t 0.503333 /Users/name/images/output.mp4



And if I then run
fluent-ffmpeg
'sffmpeg.ffprobe(videoPath)
I get :

metadata {
 streams: [
 {
 index: 0,
 codec_name: 'h264',
 codec_long_name: 'H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10',
 profile: 'High',
 codec_type: 'video',
 codec_tag_string: 'avc1',
 codec_tag: '0x31637661',
 width: 1080,
 height: 1920,
 coded_width: 1080,
 coded_height: 1920,
 closed_captions: 0,
 has_b_frames: 2,
 sample_aspect_ratio: '1:1',
 display_aspect_ratio: '9:16',
 pix_fmt: 'yuv420p',
 level: 62,
 color_range: 'unknown',
 color_space: 'unknown',
 color_transfer: 'unknown',
 color_primaries: 'unknown',
 chroma_location: 'left',
 field_order: 'unknown',
 refs: 1,
 is_avc: 'true',
 nal_length_size: 4,
 id: 'N/A',
 r_frame_rate: '27000/1001',
 avg_frame_rate: '27000/1001',
 time_base: '1/27000',
 start_pts: 0,
 start_time: 0,
 duration_ts: 14014,
 duration: 0.519037,
 bit_rate: 52138429,
 max_bit_rate: 'N/A',
 bits_per_raw_sample: 8,
 nb_frames: 14,
 nb_read_frames: 'N/A',
 nb_read_packets: 'N/A',
 tags: [Object],
 disposition: [Object]
 }
 ],
 format: {
 filename: '/Users/name/images/output.mp4',
 nb_streams: 1,
 nb_programs: 0,
 format_name: 'mov,mp4,m4a,3gp,3g2,mj2',
 format_long_name: 'QuickTime / MOV',
 start_time: 0,
 duration: 0.52,
 size: 3383708,
 bit_rate: 52057046,
 probe_score: 100,
 tags: {
 major_brand: 'isom',
 minor_version: '512',
 compatible_brands: 'isomiso2avc1mp41',
 encoder: 'Lavf60.3.100'
 }
 },
 chapters: []
}



and here is from running
ffprobe
directly :

ffprobe '/Users/name/images/output.mp4'
ffprobe version 6.1.1 Copyright (c) 2007-2023 the FFmpeg developers
 built with Apple clang version 15.0.0 (clang-1500.1.0.2.5)
 configuration: --prefix=/opt/homebrew/Cellar/ffmpeg/6.1.1_2 --enable-shared --enable-pthreads --enable-version3 --cc=clang --host-cflags= --host-ldflags='-Wl,-ld_classic' --enable-ffplay --enable-gnutls --enable-gpl --enable-libaom --enable-libaribb24 --enable-libbluray --enable-libdav1d --enable-libharfbuzz --enable-libjxl --enable-libmp3lame --enable-libopus --enable-librav1e --enable-librist --enable-librubberband --enable-libsnappy --enable-libsrt --enable-libssh --enable-libsvtav1 --enable-libtesseract --enable-libtheora --enable-libvidstab --enable-libvmaf --enable-libvorbis --enable-libvpx --enable-libwebp --enable-libx264 --enable-libx265 --enable-libxml2 --enable-libxvid --enable-lzma --enable-libfontconfig --enable-libfreetype --enable-frei0r --enable-libass --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libopenjpeg --enable-libopenvino --enable-libspeex --enable-libsoxr --enable-libzmq --enable-libzimg --disable-libjack --disable-indev=jack --enable-videotoolbox --enable-audiotoolbox --enable-neon
 libavutil 58. 29.100 / 58. 29.100
 libavcodec 60. 31.102 / 60. 31.102
 libavformat 60. 16.100 / 60. 16.100
 libavdevice 60. 3.100 / 60. 3.100
 libavfilter 9. 12.100 / 9. 12.100
 libswscale 7. 5.100 / 7. 5.100
 libswresample 4. 12.100 / 4. 12.100
 libpostproc 57. 3.100 / 57. 3.100
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from '/Users/name/images/output.mp4':
 Metadata:
 major_brand : isom
 minor_version : 512
 compatible_brands: isomiso2avc1mp41
 encoder : Lavf60.3.100
 Duration: 00:00:00.52, start: 0.000000, bitrate: 52153 kb/s
 Stream #0:0[0x1](und): Video: h264 (High) (avc1 / 0x31637661), yuv420p(progressive), 1080x1920 [SAR 1:1 DAR 9:16], 52138 kb/s, 26.97 fps, 26.97 tbr, 27k tbn (default)
 Metadata:
 handler_name : VideoHandler
 vendor_id : [0][0][0][0]
 encoder : Lavc60.3.100 libx264



This seems like it should be a fairly common scenario, but I have not been able to find examples of this, and the other questions about incorrect durations on Stack Overflow are about bigger differences (i.e. 3 seconds instead of 10 seconds : Wrong video duration when recording with ffmpeg).


Some other details :


- 

- I am running this through a Node.js script with
fluent-ffmpeg
, but I have also tried running the commands directly in the terminal and the result is the same. - I am fine with the output frames being .png / .jpg / other formats.
- I am fine with setting this to a different frame rate than the original as long as the two output videos end up with the same duration.
- One suspicious thing is that I set
-t 0.503333
when creating the video, but it doesn't seem to work as the result video showsduration: 0.519037
/00:00:00.52
.










Thank you for any help !


- I am running this through a Node.js script with