hydrus/include/HydrusVideoHandling.py

376 lines
9.9 KiB
Python
Executable File

import HydrusConstants as HC
import HydrusData
import HydrusExceptions
import HydrusImageHandling
import HydrusThreading
import matroska
import numpy
import os
import re
import subprocess
import sys
import tempfile
import traceback
import threading
import time
if HC.PLATFORM_LINUX or HC.PLATFORM_OSX:
FFMPEG_PATH = os.path.join( HC.BIN_DIR, 'ffmpeg' )
if not os.path.exists( FFMPEG_PATH ):
FFMPEG_PATH = 'ffmpeg'
elif HC.PLATFORM_WINDOWS:
FFMPEG_PATH = os.path.join( HC.BIN_DIR, 'ffmpeg.exe' )
if not os.path.exists( FFMPEG_PATH ):
FFMPEG_PATH = 'ffmpeg.exe'
def GetFFMPEGVideoProperties( path ):
info = Hydrusffmpeg_parse_infos( path )
( w, h ) = info[ 'video_size' ]
duration_in_s = info[ 'duration' ]
duration = int( duration_in_s * 1000 )
num_frames = info[ 'video_nframes' ]
return ( ( w, h ), duration, num_frames )
def GetMatroskaOrWebm( path ):
tags = matroska.parse( path )
ebml = tags[ 'EBML' ][0]
if ebml[ 'DocType' ][0] == 'matroska': return HC.VIDEO_MKV
elif ebml[ 'DocType' ][0] == 'webm': return HC.VIDEO_WEBM
raise Exception()
def GetMatroskaOrWebMProperties( path ):
tags = matroska.parse( path )
segment = tags['Segment'][0]
info = segment['Info'][0]
duration = int( ( info['Duration'][0] * info['TimecodeScale'][0] / 1e9 ) * 1000 )
tracks = segment['Tracks'][0]
trackentries = tracks['TrackEntry']
for trackentry in trackentries:
if 'Video' in trackentry:
video = trackentry['Video'][0]
width = video[ 'PixelWidth' ][0]
height = video[ 'PixelHeight' ][0]
break
num_frames = 0
return ( ( width, height ), duration, num_frames )
def HasVideoStream( path ):
try:
info = Hydrusffmpeg_parse_infos( path )
except IOError as e:
HydrusData.ShowException( 'Determining the mime for the file at ' + path + ' caused the following problem:' )
HydrusData.ShowException( e )
return False
return info[ 'video_found' ]
# this is cribbed from moviepy
def Hydrusffmpeg_parse_infos(filename, print_infos=False):
"""Get file infos using ffmpeg.
Returns a dictionnary with the fields:
"video_found", "video_fps", "duration", "video_nframes",
"video_duration"
"audio_found", "audio_fps"
"video_duration" is slightly smaller than "duration" to avoid
fetching the uncomplete frames at the end, which raises an error.
"""
# open the file in a pipe, provoke an error, read output
cmd = [ FFMPEG_PATH, "-i", filename ]
is_GIF = filename.endswith('.gif')
if is_GIF:
if HC.PLATFORM_WINDOWS: cmd += ["-f", "null", "NUL"]
else: cmd += ["-f", "null", "/dev/null"]
proc = subprocess.Popen( cmd, bufsize=10**5, stdout=subprocess.PIPE, stderr=subprocess.PIPE, startupinfo = HydrusData.GetSubprocessStartupInfo() )
infos = proc.stderr.read().decode('utf8')
proc.terminate()
del proc
if print_infos:
# print the whole info text returned by FFMPEG
HydrusData.Print( infos )
lines = infos.splitlines()
if "No such file or directory" in lines[-1]:
raise IOError("%s not found ! Wrong path ?"%filename)
result = dict()
# get duration (in seconds)
# Duration: 00:00:02.46, start: 0.033000, bitrate: 1069 kb/s
try:
keyword = ('frame=' if is_GIF else 'Duration: ')
line = [l for l in lines if keyword in l][0]
if 'start:' in line:
m = re.search( '(start\\: )' + '-?[0-9]\\.[0-9]*', line )
start_offset = float( line[ m.start() + 7 : m.end() ] )
else: start_offset = 0
match = re.search("[0-9][0-9]:[0-9][0-9]:[0-9][0-9].[0-9][0-9]", line)
hms = map(float, line[match.start()+1:match.end()].split(':'))
if len(hms) == 1:
result['duration'] = hms[0]
elif len(hms) == 2:
result['duration'] = 60*hms[0]+hms[1]
elif len(hms) ==3:
result['duration'] = 3600*hms[0]+60*hms[1]+hms[2]
result[ 'duration' ] -= start_offset
except:
raise IOError("Error reading duration in file %s,"%(filename)+
"Text parsed: %s"%infos)
# get the output line that speaks about video
lines_video = [l for l in lines if ' Video: ' in l]
result['video_found'] = ( lines_video != [] )
if result['video_found']:
line = lines_video[0]
# get the size, of the form 460x320 (w x h)
match = re.search(" [0-9]*x[0-9]*(,| )", line)
s = list(map(int, line[match.start():match.end()-1].split('x')))
result['video_size'] = s
# get the frame rate
try:
match = re.search("( [0-9]*.| )[0-9]* tbr", line)
result['video_fps'] = float(line[match.start():match.end()].split(' ')[1])
except:
match = re.search("( [0-9]*.| )[0-9]* fps", line)
result['video_fps'] = float(line[match.start():match.end()].split(' ')[1])
num_frames = result['duration'] * result['video_fps']
if num_frames != int( num_frames ): num_frames += 1 # rounding up
result['video_nframes'] = int( num_frames )
result['video_duration'] = result['duration']
# We could have also recomputed the duration from the number
# of frames, as follows:
# >>> result['video_duration'] = result['video_nframes'] / result['video_fps']
lines_audio = [l for l in lines if ' Audio: ' in l]
result['audio_found'] = lines_audio != []
if result['audio_found']:
line = lines_audio[0]
try:
match = re.search(" [0-9]* Hz", line)
result['audio_fps'] = int(line[match.start()+1:match.end()])
except:
result['audio_fps'] = 'unknown'
return result
# This was built from moviepy's FFMPEG_VideoReader
class VideoRendererFFMPEG( object ):
def __init__( self, path, mime, duration, num_frames, target_resolution, pix_fmt = "rgb24" ):
self._path = path
self._mime = mime
self._duration = float( duration ) / 1000.0
self._num_frames = num_frames
self._target_resolution = target_resolution
self.lastread = None
self.fps = float( self._num_frames ) / self._duration
if self.fps == 0:
self.fps = 24
self.pix_fmt = pix_fmt
if pix_fmt == 'rgba': self.depth = 4
else: self.depth = 3
( x, y ) = self._target_resolution
bufsize = self.depth * x * y
self.process = None
self.bufsize = bufsize
self.initialize()
def __del__( self ):
self.close()
def close(self):
if self.process is not None:
self.process.terminate()
self.process.stdout.close()
self.process.stderr.close()
self.process = None
def initialize( self, start_index = 0 ):
self.close()
if self._mime == HC.IMAGE_GIF:
ss = 0
self.pos = 0
skip_frames = start_index
else:
ss = float( start_index ) / self.fps
self.pos = start_index
skip_frames = 0
( w, h ) = self._target_resolution
cmd = [ FFMPEG_PATH,
'-ss', "%.03f" % ss,
'-i', self._path,
'-loglevel', 'quiet',
'-f', 'image2pipe',
"-pix_fmt", self.pix_fmt,
"-s", str( w ) + 'x' + str( h ),
'-vcodec', 'rawvideo', '-' ]
self.process = subprocess.Popen( cmd, bufsize = self.bufsize, stdout=subprocess.PIPE, stderr=subprocess.PIPE, startupinfo = HydrusData.GetSubprocessStartupInfo() )
self.skip_frames( skip_frames )
def skip_frames( self, n ):
( w, h ) = self._target_resolution
for i in range( n ):
if self.process is not None:
self.process.stdout.read( self.depth * w * h )
self.process.stdout.flush()
self.pos += 1
def read_frame( self ):
if self.pos == self._num_frames:
self.initialize()
if self.process is None:
result = self.lastread
else:
( w, h ) = self._target_resolution
nbytes = self.depth * w * h
s = self.process.stdout.read(nbytes)
if len(s) != nbytes:
result = self.lastread
self.close()
else:
result = numpy.fromstring( s, dtype = 'uint8' ).reshape( ( h, w, len( s ) // ( w * h ) ) )
self.lastread = result
self.pos += 1
return result
def set_position( self, pos ):
rewind = pos < self.pos
jump_a_long_way_ahead = pos > self.pos + 60
if rewind or jump_a_long_way_ahead: self.initialize( pos )
else: self.skip_frames( pos - self.pos )