2020-07-29 20:52:44 +00:00
import numpy
import os
import re
2021-10-06 20:59:30 +00:00
import struct
2020-07-29 20:52:44 +00:00
import subprocess
2020-04-22 21:00:35 +00:00
from hydrus . core import HydrusAudioHandling
from hydrus . core import HydrusConstants as HC
from hydrus . core import HydrusData
from hydrus . core import HydrusExceptions
from hydrus . core import HydrusText
2020-06-17 21:31:54 +00:00
from hydrus . core import HydrusThreading
2013-02-19 00:11:43 +00:00
2019-06-19 22:08:48 +00:00
FFMPEG_MISSING_ERROR_PUBBED = False
FFMPEG_NO_CONTENT_ERROR_PUBBED = False
2020-07-15 20:52:09 +00:00
if HC . PLATFORM_WINDOWS :
2014-05-28 21:03:24 +00:00
2020-07-15 20:52:09 +00:00
FFMPEG_PATH = os . path . join ( HC . BIN_DIR , ' ffmpeg.exe ' )
2014-05-28 21:03:24 +00:00
2020-07-15 20:52:09 +00:00
else :
2014-05-28 21:03:24 +00:00
2020-07-15 20:52:09 +00:00
FFMPEG_PATH = os . path . join ( HC . BIN_DIR , ' ffmpeg ' )
2014-05-28 21:03:24 +00:00
2016-12-07 22:12:52 +00:00
if not os . path . exists ( FFMPEG_PATH ) :
FFMPEG_PATH = os . path . basename ( FFMPEG_PATH )
2014-05-28 21:03:24 +00:00
2017-06-28 20:23:21 +00:00
def CheckFFMPEGError ( lines ) :
2018-06-20 20:20:22 +00:00
if len ( lines ) == 0 :
2020-05-27 21:27:52 +00:00
raise HydrusExceptions . DamagedOrUnusualFileException ( ' Could not parse that file--no FFMPEG output given. ' )
2018-06-20 20:20:22 +00:00
2017-06-28 20:23:21 +00:00
if " No such file or directory " in lines [ - 1 ] :
raise IOError ( " File not found! " )
if ' Invalid data ' in lines [ - 1 ] :
2020-05-27 21:27:52 +00:00
raise HydrusExceptions . DamagedOrUnusualFileException ( ' FFMPEG could not parse. ' )
2017-06-28 20:23:21 +00:00
2021-11-03 20:49:56 +00:00
def GetAPNGACTLChunk ( file_header_bytes : bytes ) :
2021-10-06 20:59:30 +00:00
2021-11-03 20:49:56 +00:00
apng_actl_chunk_header = b ' acTL '
apng_phys_chunk_header = b ' pHYs '
first_guess_header = file_header_bytes [ 37 : 128 ]
if first_guess_header . startswith ( apng_actl_chunk_header ) :
return first_guess_header
elif first_guess_header . startswith ( apng_phys_chunk_header ) :
# aha, some weird other png chunk
# https://wiki.mozilla.org/APNG_Specification
if apng_actl_chunk_header in first_guess_header :
i = first_guess_header . index ( apng_actl_chunk_header )
return first_guess_header [ i : ]
return None
def GetAPNGNumFrames ( apng_actl_bytes ) :
( num_frames , ) = struct . unpack ( ' >I ' , apng_actl_bytes [ 4 : 8 ] )
2021-10-06 20:59:30 +00:00
return num_frames
2016-12-14 21:19:07 +00:00
def GetFFMPEGVersion ( ) :
2017-11-15 22:35:49 +00:00
2016-12-14 21:19:07 +00:00
cmd = [ FFMPEG_PATH , ' -version ' ]
2020-06-17 21:31:54 +00:00
HydrusData . CheckProgramIsNotShuttingDown ( )
2016-12-14 21:19:07 +00:00
try :
2019-01-16 22:40:53 +00:00
sbp_kwargs = HydrusData . GetSubprocessKWArgs ( text = True )
2019-01-09 22:59:03 +00:00
2019-10-16 20:47:55 +00:00
process = subprocess . Popen ( cmd , stdin = subprocess . PIPE , stdout = subprocess . PIPE , stderr = subprocess . PIPE , * * sbp_kwargs )
2019-01-09 22:59:03 +00:00
except FileNotFoundError :
2019-10-09 22:03:03 +00:00
return ' no ffmpeg found at path " {} " ' . format ( FFMPEG_PATH )
2016-12-14 21:19:07 +00:00
except Exception as e :
2019-01-09 22:59:03 +00:00
HydrusData . ShowException ( e )
2019-10-09 22:03:03 +00:00
return ' unable to execute ffmpeg at path " {} " ' . format ( FFMPEG_PATH )
2016-12-14 21:19:07 +00:00
2020-06-17 21:31:54 +00:00
( stdout , stderr ) = HydrusThreading . SubprocessCommunicate ( process )
2016-12-14 21:19:07 +00:00
2019-06-19 22:08:48 +00:00
del process
2016-12-14 21:19:07 +00:00
2019-06-19 22:08:48 +00:00
lines = stdout . splitlines ( )
2016-12-14 21:19:07 +00:00
if len ( lines ) > 0 :
# typically 'ffmpeg version [VERSION] Copyright ...
top_line = lines [ 0 ]
if top_line . startswith ( ' ffmpeg version ' ) :
top_line = top_line . replace ( ' ffmpeg version ' , ' ' )
if ' ' in top_line :
version_string = top_line . split ( ' ' ) [ 0 ]
return version_string
2019-06-05 19:42:39 +00:00
message = ' FFMPEG was recently contacted to fetch version information. While FFMPEG could be found, the response could not be understood. Significant debug information has been printed to the log, which hydrus_dev would be interested in. '
HydrusData . ShowText ( message )
message + = os . linesep * 2
message + = str ( sbp_kwargs )
message + = os . linesep * 2
message + = str ( os . environ )
message + = os . linesep * 2
2019-06-19 22:08:48 +00:00
message + = ' STDOUT Response: {} ' . format ( stdout )
message + = os . linesep * 2
message + = ' STDERR Response: {} ' . format ( stderr )
2019-06-05 19:42:39 +00:00
HydrusData . Print ( message )
2019-06-19 22:08:48 +00:00
global FFMPEG_NO_CONTENT_ERROR_PUBBED
FFMPEG_NO_CONTENT_ERROR_PUBBED = True
2016-12-14 21:19:07 +00:00
return ' unknown '
2017-06-28 20:23:21 +00:00
# bits of this were originally cribbed from moviepy
2019-04-24 22:18:50 +00:00
def GetFFMPEGInfoLines ( path , count_frames_manually = False , only_first_second = False ) :
2017-06-28 20:23:21 +00:00
# open the file in a pipe, provoke an error, read output
cmd = [ FFMPEG_PATH , " -i " , path ]
2019-04-24 22:18:50 +00:00
if only_first_second :
cmd . insert ( 1 , ' -t ' )
cmd . insert ( 2 , ' 1 ' )
2017-06-28 20:23:21 +00:00
if count_frames_manually :
2019-03-27 22:01:02 +00:00
# added -an here to remove audio component, which was sometimes causing convert fails on single-frame music webms
2017-06-28 20:23:21 +00:00
if HC . PLATFORM_WINDOWS :
2019-04-24 22:18:50 +00:00
cmd + = [ " -vf " , " scale=-2:120 " , " -an " , " -f " , " null " , " NUL " ]
2017-06-28 20:23:21 +00:00
else :
2019-04-24 22:18:50 +00:00
cmd + = [ " -vf " , " scale=-2:120 " , " -an " , " -f " , " null " , " /dev/null " ]
2017-06-28 20:23:21 +00:00
2019-02-06 22:41:35 +00:00
sbp_kwargs = HydrusData . GetSubprocessKWArgs ( )
2017-06-28 20:23:21 +00:00
2020-06-17 21:31:54 +00:00
HydrusData . CheckProgramIsNotShuttingDown ( )
2019-03-20 21:22:10 +00:00
try :
2019-10-16 20:47:55 +00:00
process = subprocess . Popen ( cmd , bufsize = 10 * * 5 , stdin = subprocess . PIPE , stdout = subprocess . PIPE , stderr = subprocess . PIPE , * * sbp_kwargs )
2019-03-20 21:22:10 +00:00
except FileNotFoundError as e :
2019-05-15 20:35:00 +00:00
global FFMPEG_MISSING_ERROR_PUBBED
if not FFMPEG_MISSING_ERROR_PUBBED :
message = ' FFMPEG, which hydrus uses to parse and render video, was not found! This may be due to it not being available on your system, or hydrus being unable to find it. '
message + = os . linesep * 2
if HC . PLATFORM_WINDOWS :
message + = ' You are on Windows, so there should be a copy of ffmpeg.exe in your install_dir/bin folder. If not, please check if your anti-virus has removed it and restore it through a new install. '
else :
message + = ' If you are certain that FFMPEG is installed on your OS and accessible in your PATH, please let hydrus_dev know, as this problem is likely due to an environment problem. You may be able to solve this problem immediately by putting a static build of the ffmpeg executable in your install_dir/bin folder. '
message + = os . linesep * 2
message + = ' You can check your current FFMPEG status through help->about. '
HydrusData . ShowText ( message )
FFMPEG_MISSING_ERROR_PUBBED = True
raise FileNotFoundError ( ' Cannot interact with video because FFMPEG not found--are you sure it is installed? Full error: ' + str ( e ) )
2019-03-20 21:22:10 +00:00
2017-06-28 20:23:21 +00:00
2020-06-17 21:31:54 +00:00
( stdout , stderr ) = HydrusThreading . SubprocessCommunicate ( process )
2019-06-19 22:08:48 +00:00
data_bytes = stderr
2017-06-28 20:23:21 +00:00
2019-06-19 22:08:48 +00:00
if len ( data_bytes ) == 0 :
global FFMPEG_NO_CONTENT_ERROR_PUBBED
if not FFMPEG_NO_CONTENT_ERROR_PUBBED :
message = ' FFMPEG, which hydrus uses to parse and render video, did not return any data on a recent file metadata check! More debug info has been written to the log. '
message + = os . linesep * 2
message + = ' You can check this info again through help->about. '
HydrusData . ShowText ( message )
message + = os . linesep * 2
message + = str ( sbp_kwargs )
message + = os . linesep * 2
message + = str ( os . environ )
message + = os . linesep * 2
message + = ' STDOUT Response: {} ' . format ( stdout )
message + = os . linesep * 2
message + = ' STDERR Response: {} ' . format ( stderr )
HydrusData . DebugPrint ( message )
FFMPEG_NO_CONTENT_ERROR_PUBBED = True
raise HydrusExceptions . DataMissing ( ' Cannot interact with video because FFMPEG did not return any content. ' )
2017-06-28 20:23:21 +00:00
2019-06-19 22:08:48 +00:00
del process
2017-06-28 20:23:21 +00:00
2019-02-13 22:26:43 +00:00
( text , encoding ) = HydrusText . NonFailingUnicodeDecode ( data_bytes , ' utf-8 ' )
2019-02-06 22:41:35 +00:00
lines = text . splitlines ( )
2017-06-28 20:23:21 +00:00
2019-07-10 22:38:30 +00:00
CheckFFMPEGError ( lines )
2017-06-28 20:23:21 +00:00
return lines
2021-10-06 20:59:30 +00:00
def GetFFMPEGAPNGProperties ( path ) :
with open ( path , ' rb ' ) as f :
file_header_bytes = f . read ( 256 )
2021-11-03 20:49:56 +00:00
apng_actl_bytes = GetAPNGACTLChunk ( file_header_bytes )
if apng_actl_bytes is None :
raise HydrusExceptions . DamagedOrUnusualFileException ( ' This APNG had an unusual file header! ' )
num_frames = GetAPNGNumFrames ( apng_actl_bytes )
2021-10-06 20:59:30 +00:00
lines = GetFFMPEGInfoLines ( path )
2021-11-03 20:49:56 +00:00
resolution = ParseFFMPEGVideoResolution ( lines , png_ok = True )
2021-10-06 20:59:30 +00:00
2021-11-03 20:49:56 +00:00
( fps , confident_fps ) = ParseFFMPEGFPS ( lines , png_ok = True )
2021-10-06 20:59:30 +00:00
if not confident_fps :
fps = 24
duration = num_frames / fps
duration_in_ms = int ( duration * 1000 )
has_audio = False
return ( resolution , duration_in_ms , num_frames , has_audio )
2019-04-24 22:18:50 +00:00
def GetFFMPEGVideoProperties ( path , force_count_frames_manually = False ) :
2014-06-25 20:37:06 +00:00
2020-09-16 20:46:54 +00:00
lines_for_first_second = GetFFMPEGInfoLines ( path , count_frames_manually = True , only_first_second = True )
2014-06-25 20:37:06 +00:00
2020-09-16 20:46:54 +00:00
( has_video , video_format ) = ParseFFMPEGVideoFormat ( lines_for_first_second )
2020-01-02 03:05:35 +00:00
if not has_video :
2017-06-28 20:23:21 +00:00
2020-05-27 21:27:52 +00:00
raise HydrusExceptions . DamagedOrUnusualFileException ( ' File did not appear to have a video stream! ' )
2017-06-28 20:23:21 +00:00
2014-06-25 20:37:06 +00:00
2020-09-16 20:46:54 +00:00
resolution = ParseFFMPEGVideoResolution ( lines_for_first_second )
2014-06-25 20:37:06 +00:00
2020-09-16 20:46:54 +00:00
( file_duration_in_s , stream_duration_in_s ) = ParseFFMPEGDuration ( lines_for_first_second )
2019-04-24 22:18:50 +00:00
# this will have to be fixed when I add audio, and dynamically accounted for on dual vid/audio rendering
duration = stream_duration_in_s
2014-06-25 20:37:06 +00:00
2020-09-16 20:46:54 +00:00
( fps , confident_fps ) = ParseFFMPEGFPS ( lines_for_first_second )
2019-04-24 22:18:50 +00:00
if duration is None and not confident_fps :
2017-06-28 20:23:21 +00:00
2019-04-24 22:18:50 +00:00
# ok default to fall back on
( fps , confident_fps ) = ( 24 , True )
2017-06-28 20:23:21 +00:00
2019-04-24 22:18:50 +00:00
2020-05-06 21:31:41 +00:00
if fps is None or fps == 0 :
fps = 1
2019-05-15 20:35:00 +00:00
if duration is None :
force_count_frames_manually = True
else :
num_frames_estimate = int ( duration * fps )
# if file is big or long, don't try to force a manual count when one not explicitly asked for
# we don't care about a dropped frame on a 10min vid tbh
2020-09-16 20:46:54 +00:00
num_frames_seems_ok_to_count = duration < 15 or num_frames_estimate < 2400
2019-05-15 20:35:00 +00:00
file_is_ok_size = os . path . getsize ( path ) < 128 * 1024 * 1024
if num_frames_seems_ok_to_count and file_is_ok_size :
last_frame_has_unusual_duration = num_frames_estimate != duration * fps
unusual_video_start = file_duration_in_s != stream_duration_in_s
if not confident_fps or last_frame_has_unusual_duration or unusual_video_start :
force_count_frames_manually = True
2019-04-24 22:18:50 +00:00
2019-05-15 20:35:00 +00:00
if force_count_frames_manually :
2017-06-28 20:23:21 +00:00
2019-04-24 22:18:50 +00:00
lines = GetFFMPEGInfoLines ( path , count_frames_manually = True )
2017-06-28 20:23:21 +00:00
num_frames = ParseFFMPEGNumFramesManually ( lines )
2019-04-24 22:18:50 +00:00
if duration is None :
2017-06-28 20:23:21 +00:00
2019-04-24 22:18:50 +00:00
duration = num_frames / fps
2017-06-28 20:23:21 +00:00
2019-04-24 22:18:50 +00:00
else :
num_frames = int ( duration * fps )
2017-06-28 20:23:21 +00:00
duration_in_ms = int ( duration * 1000 )
2014-06-25 20:37:06 +00:00
2021-08-18 21:10:01 +00:00
has_audio = VideoHasAudio ( path , lines_for_first_second )
return ( resolution , duration_in_ms , num_frames , has_audio )
2014-06-25 20:37:06 +00:00
2017-06-28 20:23:21 +00:00
def GetMime ( path ) :
2016-08-17 20:07:22 +00:00
2017-06-28 20:23:21 +00:00
lines = GetFFMPEGInfoLines ( path )
2016-08-17 20:07:22 +00:00
2017-06-28 20:23:21 +00:00
try :
2016-08-17 20:07:22 +00:00
2017-06-28 20:23:21 +00:00
mime_text = ParseFFMPEGMimeText ( lines )
2016-08-17 20:07:22 +00:00
2020-05-27 21:27:52 +00:00
except HydrusExceptions . UnsupportedFileException :
2016-02-24 21:42:54 +00:00
2017-06-28 20:23:21 +00:00
return HC . APPLICATION_UNKNOWN
2016-02-24 21:42:54 +00:00
2017-06-28 20:23:21 +00:00
2020-01-02 03:05:35 +00:00
( has_video , video_format ) = ParseFFMPEGVideoFormat ( lines )
( has_audio , audio_format ) = HydrusAudioHandling . ParseFFMPEGAudio ( lines )
2017-06-28 20:23:21 +00:00
if ' matroska ' in mime_text or ' webm ' in mime_text :
2016-06-01 20:04:15 +00:00
2019-01-09 22:59:03 +00:00
# a webm has at least vp8/vp9 video and optionally vorbis audio
2016-06-01 20:04:15 +00:00
2019-01-09 22:59:03 +00:00
has_webm_video = False
2019-03-06 23:06:22 +00:00
has_webm_audio = False
2019-01-09 22:59:03 +00:00
2020-01-02 03:05:35 +00:00
if has_video :
2019-01-09 22:59:03 +00:00
webm_video_formats = ( ' vp8 ' , ' vp9 ' )
has_webm_video = True in ( webm_video_format in video_format for webm_video_format in webm_video_formats )
2019-03-06 23:06:22 +00:00
if has_audio :
2019-01-09 22:59:03 +00:00
2019-03-06 23:06:22 +00:00
webm_audio_formats = ( ' vorbis ' , ' opus ' )
has_webm_audio = True in ( webm_audio_format in audio_format for webm_audio_format in webm_audio_formats )
2019-01-09 22:59:03 +00:00
2019-03-13 21:04:21 +00:00
else :
# no audio at all is not a vote against webm
has_webm_audio = True
2019-01-09 22:59:03 +00:00
if has_webm_video and has_webm_audio :
return HC . VIDEO_WEBM
else :
return HC . VIDEO_MKV
2016-02-24 21:42:54 +00:00
2017-06-28 20:23:21 +00:00
elif mime_text in ( ' mpeg ' , ' mpegvideo ' , ' mpegts ' ) :
2017-05-24 20:28:24 +00:00
2017-06-28 20:23:21 +00:00
return HC . VIDEO_MPEG
2017-05-24 20:28:24 +00:00
2017-06-28 20:23:21 +00:00
elif mime_text == ' flac ' :
2017-05-24 20:28:24 +00:00
2017-06-28 20:23:21 +00:00
return HC . AUDIO_FLAC
2017-05-24 20:28:24 +00:00
2021-07-28 21:12:00 +00:00
elif mime_text == ' wav ' :
return HC . AUDIO_WAVE
2017-06-28 20:23:21 +00:00
elif mime_text == ' mp3 ' :
2017-05-24 20:28:24 +00:00
2017-06-28 20:23:21 +00:00
return HC . AUDIO_MP3
2017-05-24 20:28:24 +00:00
2020-01-16 02:08:23 +00:00
elif mime_text == ' tta ' :
return HC . AUDIO_TRUEAUDIO
2017-07-05 21:09:28 +00:00
elif ' mp4 ' in mime_text :
2020-01-02 03:05:35 +00:00
if has_audio and ( not has_video or ' mjpeg ' in video_format ) :
return HC . AUDIO_M4A
else :
return HC . VIDEO_MP4
2017-07-05 21:09:28 +00:00
2017-06-28 20:23:21 +00:00
elif mime_text == ' ogg ' :
2016-12-07 22:12:52 +00:00
2017-06-28 20:23:21 +00:00
return HC . AUDIO_OGG
2016-12-07 22:12:52 +00:00
2020-01-16 02:08:23 +00:00
elif ' rm ' in mime_text :
if ParseFFMPEGHasVideo ( lines ) :
return HC . VIDEO_REALMEDIA
else :
return HC . AUDIO_REALMEDIA
2017-06-28 20:23:21 +00:00
elif mime_text == ' asf ' :
2016-12-07 22:12:52 +00:00
2017-06-28 20:23:21 +00:00
if ParseFFMPEGHasVideo ( lines ) :
2016-12-07 22:12:52 +00:00
2017-06-28 20:23:21 +00:00
return HC . VIDEO_WMV
2016-12-07 22:12:52 +00:00
else :
2017-06-28 20:23:21 +00:00
return HC . AUDIO_WMA
2016-12-07 22:12:52 +00:00
2014-06-18 21:53:48 +00:00
2017-06-28 20:23:21 +00:00
return HC . APPLICATION_UNKNOWN
2017-06-14 21:19:11 +00:00
2017-06-28 20:23:21 +00:00
def HasVideoStream ( path ) :
2016-08-17 20:07:22 +00:00
2017-06-28 20:23:21 +00:00
lines = GetFFMPEGInfoLines ( path )
2017-06-07 22:05:15 +00:00
2017-06-28 20:23:21 +00:00
return ParseFFMPEGHasVideo ( lines )
2014-06-18 21:53:48 +00:00
2021-07-28 21:12:00 +00:00
def RenderImageToPNGPath ( path , temp_png_path ) :
# -y to overwrite the temp path
cmd = [ FFMPEG_PATH , ' -y ' , " -i " , path , temp_png_path ]
sbp_kwargs = HydrusData . GetSubprocessKWArgs ( )
HydrusData . CheckProgramIsNotShuttingDown ( )
try :
process = subprocess . Popen ( cmd , bufsize = 10 * * 5 , stdin = subprocess . PIPE , stdout = subprocess . PIPE , stderr = subprocess . PIPE , * * sbp_kwargs )
except FileNotFoundError as e :
global FFMPEG_MISSING_ERROR_PUBBED
if not FFMPEG_MISSING_ERROR_PUBBED :
message = ' FFMPEG, which hydrus uses to parse and render video, was not found! This may be due to it not being available on your system, or hydrus being unable to find it. '
message + = os . linesep * 2
if HC . PLATFORM_WINDOWS :
message + = ' You are on Windows, so there should be a copy of ffmpeg.exe in your install_dir/bin folder. If not, please check if your anti-virus has removed it and restore it through a new install. '
else :
message + = ' If you are certain that FFMPEG is installed on your OS and accessible in your PATH, please let hydrus_dev know, as this problem is likely due to an environment problem. You may be able to solve this problem immediately by putting a static build of the ffmpeg executable in your install_dir/bin folder. '
message + = os . linesep * 2
message + = ' You can check your current FFMPEG status through help->about. '
HydrusData . ShowText ( message )
FFMPEG_MISSING_ERROR_PUBBED = True
raise FileNotFoundError ( ' Cannot interact with video because FFMPEG not found--are you sure it is installed? Full error: ' + str ( e ) )
( stdout , stderr ) = HydrusThreading . SubprocessCommunicate ( process )
2017-06-28 20:23:21 +00:00
def ParseFFMPEGDuration ( lines ) :
2014-06-18 21:53:48 +00:00
# get duration (in seconds)
2014-06-25 20:37:06 +00:00
# Duration: 00:00:02.46, start: 0.033000, bitrate: 1069 kb/s
2014-06-18 21:53:48 +00:00
try :
2017-06-28 20:23:21 +00:00
2019-10-09 22:03:03 +00:00
# had a vid with 'Duration:' in title, ha ha, so now a regex
line = [ l for l in lines if re . search ( r ' ^ \ s*Duration: ' , l ) is not None ] [ 0 ]
2017-06-28 20:23:21 +00:00
if ' Duration: N/A ' in line :
2019-04-24 22:18:50 +00:00
return ( None , None )
2017-06-28 20:23:21 +00:00
2014-06-25 20:37:06 +00:00
if ' start: ' in line :
2016-08-24 18:36:56 +00:00
m = re . search ( ' (start \\ : ) ' + ' -?[0-9]+ \\ .[0-9]* ' , line )
2014-06-25 20:37:06 +00:00
start_offset = float ( line [ m . start ( ) + 7 : m . end ( ) ] )
2016-08-24 18:36:56 +00:00
if abs ( start_offset ) > 1.0 : # once had a file with start offset of 957499 seconds jej
start_offset = 0
else :
start_offset = 0
2014-06-25 20:37:06 +00:00
2014-06-18 21:53:48 +00:00
match = re . search ( " [0-9][0-9]:[0-9][0-9]:[0-9][0-9].[0-9][0-9] " , line )
2021-04-07 21:26:45 +00:00
hms = [ float ( float_string ) for float_string in line [ match . start ( ) + 1 : match . end ( ) ] . split ( ' : ' ) ]
2014-06-18 21:53:48 +00:00
2017-06-28 20:23:21 +00:00
if len ( hms ) == 1 :
duration = hms [ 0 ]
elif len ( hms ) == 2 :
duration = 60 * hms [ 0 ] + hms [ 1 ]
elif len ( hms ) == 3 :
duration = 3600 * hms [ 0 ] + 60 * hms [ 1 ] + hms [ 2 ]
2019-01-23 22:19:16 +00:00
if duration == 0 :
2019-04-24 22:18:50 +00:00
return ( None , None )
2019-01-23 22:19:16 +00:00
2019-04-24 22:18:50 +00:00
file_duration = duration + start_offset
stream_duration = duration
2017-06-28 20:23:21 +00:00
2019-04-24 22:18:50 +00:00
return ( file_duration , stream_duration )
2017-06-28 20:23:21 +00:00
except :
2020-05-27 21:27:52 +00:00
raise HydrusExceptions . DamagedOrUnusualFileException ( ' Error reading duration! ' )
2017-06-28 20:23:21 +00:00
2021-11-03 20:49:56 +00:00
def ParseFFMPEGFPS ( lines , png_ok = False ) :
2017-06-28 20:23:21 +00:00
try :
2021-11-03 20:49:56 +00:00
line = ParseFFMPEGVideoLine ( lines , png_ok = png_ok )
2018-10-17 21:00:09 +00:00
2021-11-03 20:49:56 +00:00
( possible_results , confident ) = ParseFFMPEGFPSPossibleResults ( line )
2017-06-28 20:23:21 +00:00
2021-11-03 20:49:56 +00:00
if len ( possible_results ) == 0 :
2017-06-28 20:23:21 +00:00
2021-11-03 20:49:56 +00:00
fps = 1
confident = False
2017-06-28 20:23:21 +00:00
2021-11-03 20:49:56 +00:00
else :
2017-06-28 20:23:21 +00:00
2021-11-03 20:49:56 +00:00
fps = min ( possible_results )
2017-06-28 20:23:21 +00:00
2018-10-17 21:00:09 +00:00
2021-11-03 20:49:56 +00:00
return ( fps , confident )
2018-10-17 21:00:09 +00:00
2021-11-03 20:49:56 +00:00
except :
2018-10-17 21:00:09 +00:00
2021-11-03 20:49:56 +00:00
raise HydrusExceptions . DamagedOrUnusualFileException ( ' Error estimating framerate! ' )
2017-06-28 20:23:21 +00:00
2021-11-03 20:49:56 +00:00
def ParseFFMPEGFPSFromFirstSecond ( lines_for_first_second ) :
try :
2019-04-24 22:18:50 +00:00
2021-11-03 20:49:56 +00:00
line = ParseFFMPEGVideoLine ( lines_for_first_second )
( possible_results , confident ) = ParseFFMPEGFPSPossibleResults ( line )
num_frames_in_first_second = ParseFFMPEGNumFramesManually ( lines_for_first_second )
2019-04-24 22:18:50 +00:00
2018-10-17 21:00:09 +00:00
if len ( possible_results ) == 0 :
2019-04-24 22:18:50 +00:00
fps = num_frames_in_first_second
confident = False
2018-10-17 21:00:09 +00:00
else :
2019-05-15 20:35:00 +00:00
# in some cases, fps is 0.77 and tbr is incorrectly 20. extreme values cause bad results. let's default to slowest, but test our actual first second for most legit-looking
2018-10-17 21:00:09 +00:00
2020-09-16 20:46:54 +00:00
sensible_first_second = 1 < = num_frames_in_first_second < = 288
2019-04-24 22:18:50 +00:00
2019-05-15 20:35:00 +00:00
fps = min ( possible_results )
2019-04-24 22:18:50 +00:00
2019-05-15 20:35:00 +00:00
fps_matches_with_first_second = False
2019-04-24 22:18:50 +00:00
2019-05-15 20:35:00 +00:00
for possible_fps in possible_results :
if num_frames_in_first_second - 1 < = possible_fps and possible_fps < = num_frames_in_first_second + 1 :
fps = possible_fps
fps_matches_with_first_second = True
break
2018-10-17 21:00:09 +00:00
2019-05-15 20:35:00 +00:00
confident = sensible_first_second and fps_matches_with_first_second
2019-04-24 22:18:50 +00:00
2020-05-06 21:31:41 +00:00
if fps is None or fps == 0 :
fps = 1
confident = False
2019-04-24 22:18:50 +00:00
return ( fps , confident )
2014-06-25 20:37:06 +00:00
2014-06-18 21:53:48 +00:00
except :
2017-06-28 20:23:21 +00:00
2020-05-27 21:27:52 +00:00
raise HydrusExceptions . DamagedOrUnusualFileException ( ' Error estimating framerate! ' )
2017-06-28 20:23:21 +00:00
2021-11-03 20:49:56 +00:00
def ParseFFMPEGFPSPossibleResults ( video_line ) :
# get the frame rate
possible_results = set ( )
match = re . search ( " ( [0-9]*.| )[0-9]* tbr " , video_line )
if match is not None :
tbr = video_line [ match . start ( ) : match . end ( ) ] . split ( ' ' ) [ 1 ]
tbr_fps_is_likely_garbage = match is None or tbr . endswith ( ' k ' ) or float ( tbr ) > 144
if not tbr_fps_is_likely_garbage :
possible_results . add ( float ( tbr ) )
#
match = re . search ( " ( [0-9]*.| )[0-9]* fps " , video_line )
if match is not None :
fps = video_line [ match . start ( ) : match . end ( ) ] . split ( ' ' ) [ 1 ]
fps_is_likely_garbage = match is None or fps . endswith ( ' k ' ) or float ( fps ) > 144
if not fps_is_likely_garbage :
possible_results . add ( float ( fps ) )
possible_results . discard ( 0 )
confident = len ( possible_results ) < = 1
return ( possible_results , confident )
2017-06-28 20:23:21 +00:00
def ParseFFMPEGHasVideo ( lines ) :
try :
video_line = ParseFFMPEGVideoLine ( lines )
2020-05-27 21:27:52 +00:00
except HydrusExceptions . UnsupportedFileException :
2017-06-28 20:23:21 +00:00
return False
return True
def ParseFFMPEGMimeText ( lines ) :
2016-08-17 20:07:22 +00:00
try :
( input_line , ) = [ l for l in lines if l . startswith ( ' Input #0 ' ) ]
# Input #0, matroska, webm, from 'm.mkv':
text = input_line [ 10 : ]
mime_text = text . split ( ' , from ' ) [ 0 ]
2017-06-28 20:23:21 +00:00
return mime_text
2016-08-17 20:07:22 +00:00
except :
2020-05-27 21:27:52 +00:00
raise HydrusExceptions . DamagedOrUnusualFileException ( ' Error reading mime! ' )
2016-08-17 20:07:22 +00:00
2017-06-28 20:23:21 +00:00
def ParseFFMPEGNumFramesManually ( lines ) :
2019-05-15 20:35:00 +00:00
frame_lines = [ l for l in lines if l . startswith ( ' frame= ' ) ]
2019-04-24 22:18:50 +00:00
if len ( frame_lines ) == 0 :
2017-05-31 21:50:53 +00:00
2020-05-27 21:27:52 +00:00
raise HydrusExceptions . DamagedOrUnusualFileException ( ' Video appears to be broken and non-renderable--perhaps a damaged single-frame video? ' )
2017-05-31 21:50:53 +00:00
2019-04-24 22:18:50 +00:00
2019-05-15 20:35:00 +00:00
final_line = frame_lines [ - 1 ] # there will be many progress rows, counting up as the file renders. we hence want the final one
l = final_line
2019-04-24 22:18:50 +00:00
2019-05-15 20:35:00 +00:00
l = l . replace ( ' frame= ' , ' ' )
2019-04-24 22:18:50 +00:00
2019-05-15 20:35:00 +00:00
while l . startswith ( ' ' ) :
2017-06-28 20:23:21 +00:00
2019-05-15 20:35:00 +00:00
l = l [ 1 : ]
2019-04-24 22:18:50 +00:00
try :
2017-05-31 21:50:53 +00:00
2019-05-15 20:35:00 +00:00
frames_string = l . split ( ' ' ) [ 0 ]
2017-06-28 20:23:21 +00:00
2019-04-24 22:18:50 +00:00
num_frames = int ( frames_string )
2017-06-28 20:23:21 +00:00
except :
2020-05-27 21:27:52 +00:00
raise HydrusExceptions . DamagedOrUnusualFileException ( ' Video was unable to render correctly--could not parse ffmpeg output line: " {} " ' . format ( final_line ) )
2017-06-28 20:23:21 +00:00
2019-04-24 22:18:50 +00:00
return num_frames
2019-01-09 22:59:03 +00:00
def ParseFFMPEGVideoFormat ( lines ) :
2020-01-02 03:05:35 +00:00
try :
line = ParseFFMPEGVideoLine ( lines )
2020-05-27 21:27:52 +00:00
except HydrusExceptions . UnsupportedFileException :
2020-01-02 03:05:35 +00:00
return ( False , ' unknown ' )
2019-01-09 22:59:03 +00:00
try :
2019-08-07 22:59:53 +00:00
match = re . search ( r ' (?<=Video \ : \ s).+?(?=,) ' , line )
2019-01-09 22:59:03 +00:00
video_format = match . group ( )
except :
video_format = ' unknown '
2020-01-02 03:05:35 +00:00
return ( True , video_format )
2019-01-09 22:59:03 +00:00
2021-11-03 20:49:56 +00:00
def ParseFFMPEGVideoLine ( lines , png_ok = False ) :
if png_ok :
bad_video_formats = [ ' jpg ' ]
else :
bad_video_formats = [ ' png ' , ' jpg ' ]
2017-05-31 21:50:53 +00:00
2014-06-18 21:53:48 +00:00
# get the output line that speaks about video
2019-01-16 22:40:53 +00:00
# the ^\sStream is to exclude the 'title' line, when it exists, includes the string 'Video: ', ha ha
2021-11-03 20:49:56 +00:00
lines_video = [ l for l in lines if re . search ( r ' ^ \ s*Stream ' , l ) is not None and ' Video: ' in l and True not in ( ' Video: {} ' . format ( bad_video_format ) in l for bad_video_format in bad_video_formats ) ] # mp3 says it has a 'png' video stream
2014-06-18 21:53:48 +00:00
2017-06-28 20:23:21 +00:00
if len ( lines_video ) == 0 :
2020-05-27 21:27:52 +00:00
raise HydrusExceptions . DamagedOrUnusualFileException ( ' Could not find video information! ' )
2017-06-28 20:23:21 +00:00
line = lines_video [ 0 ]
return line
2021-11-03 20:49:56 +00:00
def ParseFFMPEGVideoResolution ( lines , png_ok = False ) :
2014-06-18 21:53:48 +00:00
2017-06-28 20:23:21 +00:00
try :
2021-11-03 20:49:56 +00:00
line = ParseFFMPEGVideoLine ( lines , png_ok = png_ok )
2014-06-18 21:53:48 +00:00
# get the size, of the form 460x320 (w x h)
match = re . search ( " [0-9]*x[0-9]*(,| ) " , line )
2014-06-25 20:37:06 +00:00
2021-04-07 21:26:45 +00:00
resolution_string = line [ match . start ( ) : match . end ( ) - 1 ]
( width_string , height_string ) = resolution_string . split ( ' x ' )
width = int ( width_string )
height = int ( height_string )
2014-06-25 20:37:06 +00:00
2020-07-29 20:52:44 +00:00
sar_match = re . search ( " [ \\ [ \\ s]SAR [0-9]*:[0-9]* " , line )
2017-11-01 20:37:39 +00:00
if sar_match is not None :
# ' SAR 2:3 '
sar_string = line [ sar_match . start ( ) : sar_match . end ( ) ]
# '2:3'
sar_string = sar_string [ 5 : - 1 ]
2021-04-07 21:26:45 +00:00
( sar_width_string , sar_height_string ) = sar_string . split ( ' : ' )
2017-11-01 20:37:39 +00:00
2021-04-07 21:26:45 +00:00
sar_width = int ( sar_width_string )
sar_height = int ( sar_height_string )
2017-11-01 20:37:39 +00:00
2021-04-07 21:26:45 +00:00
width * = sar_width
width / / = sar_height
2017-11-01 20:37:39 +00:00
2021-04-07 21:26:45 +00:00
return ( width , height )
2017-06-28 20:23:21 +00:00
except :
2020-05-27 21:27:52 +00:00
raise HydrusExceptions . DamagedOrUnusualFileException ( ' Error parsing resolution! ' )
2014-06-25 20:37:06 +00:00
2014-05-28 21:03:24 +00:00
2021-08-18 21:10:01 +00:00
def VideoHasAudio ( path , info_lines ) :
( audio_found , audio_format ) = HydrusAudioHandling . ParseFFMPEGAudio ( info_lines )
if not audio_found :
return False
# just because video metadata has an audio stream doesn't mean it has audio. some vids have silent audio streams lmao
# so, let's read it as PCM and see if there is any noise
# this obviously only works for single audio stream vids, we'll adapt this if someone discovers a multi-stream mkv with a silent channel that doesn't work here
cmd = [ FFMPEG_PATH ]
# this is perhaps not sensible for eventual playback and I should rather go for wav file-like and feed into python 'wave' in order to maintain stereo/mono and so on and have easy chunk-reading
cmd . extend ( [ ' -i ' , path ,
' -loglevel ' , ' quiet ' ,
' -f ' , ' s16le ' ,
' - ' ] )
sbp_kwargs = HydrusData . GetSubprocessKWArgs ( )
HydrusData . CheckProgramIsNotShuttingDown ( )
try :
process = subprocess . Popen ( cmd , bufsize = 65536 , stdin = subprocess . PIPE , stdout = subprocess . PIPE , stderr = subprocess . PIPE , * * sbp_kwargs )
except FileNotFoundError as e :
HydrusData . ShowText ( ' Cannot render audio--FFMPEG not found! ' )
raise
# silent PCM data is just 00 bytes
# every now and then, you'll get a couple ffs for some reason, but this is not legit audio data
try :
chunk_of_pcm_data = process . stdout . read ( 65536 )
while len ( chunk_of_pcm_data ) > 0 :
# iterating over bytes gives you ints, recall
if True in ( b != 0 and b != 255 for b in chunk_of_pcm_data ) :
return True
chunk_of_pcm_data = process . stdout . read ( 65536 )
return False
finally :
process . terminate ( )
process . stdout . close ( )
process . stderr . close ( )
2014-06-25 20:37:06 +00:00
# This was built from moviepy's FFMPEG_VideoReader
class VideoRendererFFMPEG ( object ) :
2015-11-18 22:44:07 +00:00
2014-06-25 20:37:06 +00:00
def __init__ ( self , path , mime , duration , num_frames , target_resolution , pix_fmt = " rgb24 " ) :
2014-05-28 21:03:24 +00:00
2014-06-25 20:37:06 +00:00
self . _path = path
self . _mime = mime
2019-01-09 22:59:03 +00:00
self . _duration = duration / 1000.0
2014-06-25 20:37:06 +00:00
self . _num_frames = num_frames
2014-05-28 21:03:24 +00:00
self . _target_resolution = target_resolution
2015-03-04 22:44:32 +00:00
self . lastread = None
2019-01-09 22:59:03 +00:00
self . fps = self . _num_frames / self . _duration
2014-05-28 21:03:24 +00:00
2016-07-06 21:13:15 +00:00
if self . fps == 0 :
self . fps = 24
2014-05-28 21:03:24 +00:00
2014-06-25 20:37:06 +00:00
self . pix_fmt = pix_fmt
2014-05-28 21:03:24 +00:00
2014-06-25 20:37:06 +00:00
if pix_fmt == ' rgba ' : self . depth = 4
else : self . depth = 3
2014-05-28 21:03:24 +00:00
2014-06-25 20:37:06 +00:00
( x , y ) = self . _target_resolution
2015-11-25 22:00:57 +00:00
2014-06-25 20:37:06 +00:00
bufsize = self . depth * x * y
2014-05-28 21:03:24 +00:00
2014-06-25 20:37:06 +00:00
self . process = None
2014-05-28 21:03:24 +00:00
2014-06-25 20:37:06 +00:00
self . bufsize = bufsize
2014-05-28 21:03:24 +00:00
2014-06-25 20:37:06 +00:00
self . initialize ( )
2014-05-28 21:03:24 +00:00
2014-06-25 20:37:06 +00:00
2019-03-06 23:06:22 +00:00
def close ( self ) :
2014-05-28 21:03:24 +00:00
2014-06-25 20:37:06 +00:00
if self . process is not None :
2014-05-28 21:03:24 +00:00
2014-06-25 20:37:06 +00:00
self . process . terminate ( )
2014-05-28 21:03:24 +00:00
2014-06-25 20:37:06 +00:00
self . process . stdout . close ( )
self . process . stderr . close ( )
2014-05-28 21:03:24 +00:00
2014-06-25 20:37:06 +00:00
self . process = None
2014-05-28 21:03:24 +00:00
2014-06-25 20:37:06 +00:00
def initialize ( self , start_index = 0 ) :
2014-05-28 21:03:24 +00:00
2014-06-25 20:37:06 +00:00
self . close ( )
2017-06-28 20:23:21 +00:00
if self . _mime in ( HC . IMAGE_APNG , HC . IMAGE_GIF ) :
2014-05-28 21:03:24 +00:00
2018-02-07 23:40:33 +00:00
do_ss = False
2014-06-25 20:37:06 +00:00
ss = 0
self . pos = 0
skip_frames = start_index
2014-05-28 21:03:24 +00:00
2014-06-25 20:37:06 +00:00
else :
2014-05-28 21:03:24 +00:00
2018-02-07 23:40:33 +00:00
if start_index == 0 :
2018-02-14 21:47:18 +00:00
do_ss = False
2018-02-07 23:40:33 +00:00
else :
2018-02-14 21:47:18 +00:00
do_ss = True
2018-02-07 23:40:33 +00:00
2019-01-09 22:59:03 +00:00
ss = start_index / self . fps
2014-06-25 20:37:06 +00:00
self . pos = start_index
skip_frames = 0
2014-05-28 21:03:24 +00:00
2021-11-03 20:49:56 +00:00
do_fast_seek = True
2014-06-25 20:37:06 +00:00
( w , h ) = self . _target_resolution
2014-05-28 21:03:24 +00:00
2018-02-07 23:40:33 +00:00
cmd = [ FFMPEG_PATH ]
2021-11-03 20:49:56 +00:00
if do_ss and do_fast_seek : # fast seek
cmd . extend ( [ ' -ss ' , " %.03f " % ss ] )
cmd . extend ( [ ' -i ' , self . _path ] )
if do_ss and not do_fast_seek : # slow seek
2018-02-07 23:40:33 +00:00
cmd . extend ( [ ' -ss ' , " %.03f " % ss ] )
2021-11-03 20:49:56 +00:00
cmd . extend ( [
2014-06-25 20:37:06 +00:00
' -loglevel ' , ' quiet ' ,
' -f ' , ' image2pipe ' ,
" -pix_fmt " , self . pix_fmt ,
" -s " , str ( w ) + ' x ' + str ( h ) ,
2017-05-31 21:50:53 +00:00
' -vsync ' , ' 0 ' ,
2019-08-07 22:59:53 +00:00
' -vcodec ' , ' rawvideo ' ,
2021-11-03 20:49:56 +00:00
' - '
] )
2015-06-17 20:01:41 +00:00
2014-05-28 21:03:24 +00:00
2019-01-09 22:59:03 +00:00
sbp_kwargs = HydrusData . GetSubprocessKWArgs ( )
2020-06-17 21:31:54 +00:00
HydrusData . CheckProgramIsNotShuttingDown ( )
2019-05-15 20:35:00 +00:00
try :
2019-10-16 20:47:55 +00:00
self . process = subprocess . Popen ( cmd , bufsize = self . bufsize , stdin = subprocess . PIPE , stdout = subprocess . PIPE , stderr = subprocess . PIPE , * * sbp_kwargs )
2019-05-15 20:35:00 +00:00
except FileNotFoundError as e :
HydrusData . ShowText ( ' Cannot render video--FFMPEG not found! ' )
raise
2014-05-28 21:03:24 +00:00
2016-07-27 21:53:34 +00:00
if skip_frames > 0 :
self . skip_frames ( skip_frames )
2014-05-28 21:03:24 +00:00
2014-06-25 20:37:06 +00:00
def skip_frames ( self , n ) :
2014-05-28 21:03:24 +00:00
2018-11-14 23:10:55 +00:00
n = int ( n )
2014-06-25 20:37:06 +00:00
( w , h ) = self . _target_resolution
2014-05-28 21:03:24 +00:00
2014-06-25 20:37:06 +00:00
for i in range ( n ) :
2014-05-28 21:03:24 +00:00
2015-09-09 22:04:39 +00:00
if self . process is not None :
self . process . stdout . read ( self . depth * w * h )
self . process . stdout . flush ( )
2014-06-25 20:37:06 +00:00
self . pos + = 1
2014-05-28 21:03:24 +00:00
2014-06-25 20:37:06 +00:00
def read_frame ( self ) :
2016-07-20 19:57:10 +00:00
if self . pos == self . _num_frames :
self . initialize ( )
2014-05-28 21:03:24 +00:00
2016-07-06 21:13:15 +00:00
if self . process is None :
result = self . lastread
2014-06-25 20:37:06 +00:00
else :
2014-05-28 21:03:24 +00:00
2014-06-25 20:37:06 +00:00
( w , h ) = self . _target_resolution
nbytes = self . depth * w * h
2019-03-06 23:06:22 +00:00
s = self . process . stdout . read ( nbytes )
2014-06-25 20:37:06 +00:00
if len ( s ) != nbytes :
2018-01-10 22:41:51 +00:00
if self . lastread is None :
2019-03-06 23:06:22 +00:00
if self . pos != 0 :
# this renderer was asked to render starting from mid-vid and this was not possible due to broken key frame index whatever
# lets try and render from the vid start before we say the whole vid is broke
# I tried doing 'start from 0 and skip n frames', but this is super laggy so would need updates further up the pipe to display this to the user
# atm this error states does not communicate to the videocontainer that the current frame num has changed, so the frames are henceforth out of phase
2020-05-20 21:36:02 +00:00
#frames_to_jump = self.pos
2019-03-06 23:06:22 +00:00
self . set_position ( 0 )
return self . read_frame ( )
2018-01-10 22:41:51 +00:00
raise Exception ( ' Unable to render that video! Please send it to hydrus dev so he can look at it! ' )
2014-06-25 20:37:06 +00:00
result = self . lastread
2014-05-28 21:03:24 +00:00
2014-06-25 20:37:06 +00:00
self . close ( )
2014-05-28 21:03:24 +00:00
else :
2014-06-25 20:37:06 +00:00
result = numpy . fromstring ( s , dtype = ' uint8 ' ) . reshape ( ( h , w , len ( s ) / / ( w * h ) ) )
self . lastread = result
2014-05-28 21:03:24 +00:00
2014-06-25 20:37:06 +00:00
self . pos + = 1
return result
2014-05-28 21:03:24 +00:00
2014-06-25 20:37:06 +00:00
def set_position ( self , pos ) :
2014-05-28 21:03:24 +00:00
2014-06-25 20:37:06 +00:00
rewind = pos < self . pos
jump_a_long_way_ahead = pos > self . pos + 60
2018-09-05 20:52:32 +00:00
if rewind or jump_a_long_way_ahead :
self . initialize ( pos )
else :
self . skip_frames ( pos - self . pos )
2014-05-28 21:03:24 +00:00
2016-12-07 22:12:52 +00:00
2019-03-06 23:06:22 +00:00
def Stop ( self ) :
self . close ( )