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
2014-06-18 21:53:48 +00:00
import numpy
2013-07-10 20:25:57 +00:00
import os
2014-06-18 21:53:48 +00:00
import re
import subprocess
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
2019-11-20 23:10:46 +00:00
if HC . PLATFORM_LINUX or HC . PLATFORM_MACOS :
2014-05-28 21:03:24 +00:00
2015-11-18 22:44:07 +00:00
FFMPEG_PATH = os . path . join ( HC . BIN_DIR , ' ffmpeg ' )
2014-05-28 21:03:24 +00:00
2015-11-18 22:44:07 +00:00
elif HC . PLATFORM_WINDOWS :
2014-05-28 21:03:24 +00:00
2015-11-18 22:44:07 +00:00
FFMPEG_PATH = os . path . join ( HC . BIN_DIR , ' ffmpeg.exe ' )
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
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 ' ]
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
2019-06-19 22:08:48 +00:00
( stdout , stderr ) = process . communicate ( )
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
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
2019-06-19 22:08:48 +00:00
( stdout , stderr ) = process . communicate ( )
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
2019-04-24 22:18:50 +00:00
def GetFFMPEGVideoProperties ( path , force_count_frames_manually = False ) :
2014-06-25 20:37:06 +00:00
2019-05-15 20:35:00 +00:00
first_second_lines = GetFFMPEGInfoLines ( path , count_frames_manually = True , only_first_second = True )
2014-06-25 20:37:06 +00:00
2020-01-02 03:05:35 +00:00
( has_video , video_format ) = ParseFFMPEGVideoFormat ( first_second_lines )
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
2019-05-15 20:35:00 +00:00
resolution = ParseFFMPEGVideoResolution ( first_second_lines )
2014-06-25 20:37:06 +00:00
2019-05-15 20:35:00 +00:00
( file_duration_in_s , stream_duration_in_s ) = ParseFFMPEGDuration ( first_second_lines )
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
2019-05-15 20:35:00 +00:00
( fps , confident_fps ) = ParseFFMPEGFPS ( first_second_lines )
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
num_frames_inferrence_likely_odd = True # i.e. inferrence not possible!
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
num_frames_seems_ok_to_count = num_frames_estimate < 2400
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
2017-06-28 20:23:21 +00:00
return ( resolution , duration_in_ms , num_frames )
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
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
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 )
2019-01-09 22:59:03 +00:00
hms = list ( map ( float , 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
2019-05-15 20:35:00 +00:00
def ParseFFMPEGFPS ( first_second_lines ) :
2017-06-28 20:23:21 +00:00
try :
2019-05-15 20:35:00 +00:00
line = ParseFFMPEGVideoLine ( first_second_lines )
2017-06-28 20:23:21 +00:00
# get the frame rate
2019-04-24 22:18:50 +00:00
possible_results = set ( )
2018-10-17 21:00:09 +00:00
2017-06-28 20:23:21 +00:00
match = re . search ( " ( [0-9]*.| )[0-9]* tbr " , line )
if match is not None :
2018-10-17 21:00:09 +00:00
tbr = line [ match . start ( ) : match . end ( ) ] . split ( ' ' ) [ 1 ]
2017-06-28 20:23:21 +00:00
2019-04-24 22:18:50 +00:00
tbr_fps_is_likely_garbage = match is None or tbr . endswith ( ' k ' ) or float ( tbr ) > 144
2017-06-28 20:23:21 +00:00
2018-10-17 21:00:09 +00:00
if not tbr_fps_is_likely_garbage :
2017-06-28 20:23:21 +00:00
2019-04-24 22:18:50 +00:00
possible_results . add ( float ( tbr ) )
2017-06-28 20:23:21 +00:00
2018-10-17 21:00:09 +00:00
#
match = re . search ( " ( [0-9]*.| )[0-9]* fps " , line )
if match is not None :
fps = line [ match . start ( ) : match . end ( ) ] . split ( ' ' ) [ 1 ]
2019-04-24 22:18:50 +00:00
fps_is_likely_garbage = match is None or fps . endswith ( ' k ' ) or float ( fps ) > 144
2017-06-28 20:23:21 +00:00
2018-10-17 21:00:09 +00:00
if not fps_is_likely_garbage :
2017-06-28 20:23:21 +00:00
2019-04-24 22:18:50 +00:00
possible_results . add ( float ( fps ) )
2017-06-28 20:23:21 +00:00
2019-05-15 20:35:00 +00:00
num_frames_in_first_second = ParseFFMPEGNumFramesManually ( first_second_lines )
2019-04-24 22:18:50 +00:00
confident = len ( possible_results ) < = 1
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
2019-05-15 20:35:00 +00:00
sensible_first_second = num_frames_in_first_second > 1
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
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
2017-06-28 20:23:21 +00:00
def ParseFFMPEGVideoLine ( lines ) :
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
2019-08-07 22:59:53 +00:00
lines_video = [ l for l in lines if re . search ( r ' ^ \ s*Stream ' , l ) is not None and ' Video: ' in l and not ( ' Video: png ' in l or ' Video: jpg ' in l ) ] # 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
def ParseFFMPEGVideoResolution ( lines ) :
2014-06-18 21:53:48 +00:00
2017-06-28 20:23:21 +00:00
try :
line = ParseFFMPEGVideoLine ( lines )
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
2017-06-28 20:23:21 +00:00
resolution = list ( map ( int , line [ match . start ( ) : match . end ( ) - 1 ] . split ( ' x ' ) ) )
2014-06-25 20:37:06 +00:00
2017-11-01 20:37:39 +00:00
sar_match = re . search ( " SAR [0-9]*:[0-9]* " , line )
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 ]
( sar_w , sar_h ) = sar_string . split ( ' : ' )
( sar_w , sar_h ) = ( int ( sar_w ) , int ( sar_h ) )
( x , y ) = resolution
x * = sar_w
x / / = sar_h
resolution = ( x , y )
2017-06-28 20:23:21 +00:00
return resolution
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
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
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 ]
if do_ss :
cmd . extend ( [ ' -ss ' , " %.03f " % ss ] )
cmd . extend ( [ ' -i ' , self . _path ,
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 ' ,
' - ' ] )
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 ( )
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 ( )