Created
November 5, 2018 02:05
-
-
Save Revan654/bb36f17593d4a7d3e046967dc46f01ef to your computer and use it in GitHub Desktop.
VCSi with MediaInfo Support & Tweaks
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#cython: language_level=3 | |
## Copyright (c) MediaArea.net SARL. All Rights Reserved. | |
# | |
# Use of this source code is governed by a BSD-style license that can | |
# be found in the License.html file in the root of the source tree. | |
## | |
# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | |
# | |
# Public DLL interface implementation | |
# Wrapper for MediaInfo Library | |
# Please see MediaInfo.h for help | |
# | |
# Converted to python module by Petr Kaderabek | |
# Modifications by Jerome Martinez | |
# Python 3 update by Jerome Martinez | |
# Mac OSX support, Python 2/3 merge and ctypes fixes by Miguel Grinberg | |
# | |
# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/ | |
#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | |
# | |
# MediaInfoDLL.py and MediaInfoDLL3.py are same | |
# but all files are kept in order to not break programs calling them. | |
# | |
#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ | |
import os | |
import sys | |
from ctypes import * | |
if os.name == "nt" or os.name == "dos" or os.name == "os2" or os.name == "ce": | |
MediaInfoDLL_Handler = windll.MediaInfo | |
MustUseAnsi = 0 | |
elif sys.platform == "darwin": | |
MediaInfoDLL_Handler = CDLL("libmediainfo.0.dylib") | |
MustUseAnsi = 1 | |
else: | |
MediaInfoDLL_Handler = CDLL("libmediainfo.so.0") | |
MustUseAnsi = 1 | |
# types --> C Python: | |
# size_t c_size_t | |
# unsigned char* c_char_p | |
# enum c_size_t | |
# const wchar_t* c_wchar_p, | |
# NULL None, | |
# these functions need strings in unicode format | |
class Stream: | |
General, Video, Audio, Text, Other, Image, Menu, Max = list(range(8)) | |
class Info: | |
Name, Text, Measure, Options, Name_Text, Measure_Text, Info, HowTo, Max = list(range(9)) | |
class InfoOption: | |
ShowInInform, Reserved, ShowInSupported, TypeOfValue, Max = list(range(5)) | |
class FileOptions: | |
Nothing, Recursive, CloseAll, xxNonexx_3, Max = list(range(5)) | |
class MediaInfo: | |
#MEDIAINFO_EXP void* __stdcall MediaInfo_New (); /*you must ALWAYS call MediaInfo_Delete(Handle) in order to free memory*/ | |
#/** @brief A 'new' MediaInfo interface (with a quick init of useful options : "**VERSION**;**APP_NAME**;**APP_VERSION**", but without debug information, use it only if you know what you do), return a Handle, don't forget to delete it after using it*/ | |
MediaInfo_New = MediaInfoDLL_Handler.MediaInfo_New | |
MediaInfo_New.argtypes = [] | |
MediaInfo_New.restype = c_void_p | |
#MEDIAINFO_EXP void* __stdcall MediaInfo_New_Quick (const wchar_t* File, const wchar_t* Options); /*you must ALWAYS call MediaInfo_Delete(Handle) in order to free memory*/ | |
MediaInfo_New_Quick = MediaInfoDLL_Handler.MediaInfo_New_Quick | |
MediaInfo_New_Quick.argtypes = [c_wchar_p, c_wchar_p] | |
MediaInfo_New_Quick.restype = c_void_p | |
MediaInfoA_New_Quick = MediaInfoDLL_Handler.MediaInfoA_New_Quick | |
MediaInfoA_New_Quick.argtypes = [c_char_p, c_char_p] | |
MediaInfoA_New_Quick.restype = c_void_p | |
#/** @brief Delete a MediaInfo interface*/ | |
#MEDIAINFO_EXP void __stdcall MediaInfo_Delete (void* Handle); | |
MediaInfo_Delete = MediaInfoDLL_Handler.MediaInfo_Delete | |
MediaInfo_Delete.argtypes = [c_void_p] | |
MediaInfo_Delete.restype = None | |
#/** @brief Wrapper for MediaInfoLib::MediaInfo::Open (with a filename)*/ | |
#MEDIAINFO_EXP size_t __stdcall MediaInfo_Open (void* Handle, const wchar_t* File); | |
MediaInfo_Open = MediaInfoDLL_Handler.MediaInfo_Open | |
MediaInfo_Open.argtypes = [c_void_p, c_wchar_p] | |
MediaInfo_Open.restype = c_size_t | |
MediaInfoA_Open = MediaInfoDLL_Handler.MediaInfoA_Open | |
MediaInfoA_Open.argtypes = [c_void_p, c_char_p] | |
MediaInfoA_Open.restype = c_size_t | |
#/** @brief Wrapper for MediaInfoLib::MediaInfo::Open (with a buffer) */ | |
#MEDIAINFO_EXP size_t __stdcall MediaInfo_Open_Buffer (void* Handle, const unsigned char* Begin, size_t Begin_Size, const unsigned char* End, size_t End_Size); /*return Handle*/ | |
MediaInfo_Open_Buffer = MediaInfoDLL_Handler.MediaInfo_Open_Buffer | |
MediaInfo_Open_Buffer.argtypes = [c_void_p, c_void_p, c_size_t, c_void_p, c_size_t] | |
MediaInfo_Open_Buffer.restype = c_size_t | |
#/** @brief Wrapper for MediaInfoLib::MediaInfo::Open (with a buffer, Init) */ | |
#MEDIAINFO_EXP size_t __stdcall MediaInfo_Open_Buffer_Init (void* Handle, MediaInfo_int64u File_Size, MediaInfo_int64u File_Offset); | |
MediaInfo_Open_Buffer_Init = MediaInfoDLL_Handler.MediaInfo_Open_Buffer_Init | |
MediaInfo_Open_Buffer_Init.argtypes = [c_void_p, c_uint64, c_uint64] | |
MediaInfo_Open_Buffer_Init.restype = c_size_t | |
#/** @brief Wrapper for MediaInfoLib::MediaInfo::Open (with a buffer, Continue) */ | |
#MEDIAINFO_EXP size_t __stdcall MediaInfo_Open_Buffer_Continue (void* Handle, MediaInfo_int8u* Buffer, size_t Buffer_Size); | |
MediaInfo_Open_Buffer_Continue = MediaInfoDLL_Handler.MediaInfo_Open_Buffer_Continue | |
MediaInfo_Open_Buffer_Continue.argtypes = [c_void_p, c_char_p, c_size_t] | |
MediaInfo_Open_Buffer_Continue.restype = c_size_t | |
#/** @brief Wrapper for MediaInfoLib::MediaInfo::Open (with a buffer, Continue_GoTo_Get) */ | |
#MEDIAINFO_EXP MediaInfo_int64u __stdcall MediaInfo_Open_Buffer_Continue_GoTo_Get (void* Handle); | |
MediaInfo_Open_Buffer_Continue_GoTo_Get = MediaInfoDLL_Handler.MediaInfo_Open_Buffer_Continue_GoTo_Get | |
MediaInfo_Open_Buffer_Continue_GoTo_Get.argtypes = [c_void_p] | |
MediaInfo_Open_Buffer_Continue_GoTo_Get.restype = c_uint64 | |
#/** @brief Wrapper for MediaInfoLib::MediaInfo::Open (with a buffer, Finalize) */ | |
#MEDIAINFO_EXP size_t __stdcall MediaInfo_Open_Buffer_Finalize (void* Handle); | |
MediaInfo_Open_Buffer_Finalize = MediaInfoDLL_Handler.MediaInfo_Open_Buffer_Finalize | |
MediaInfo_Open_Buffer_Finalize.argtypes = [c_void_p] | |
MediaInfo_Open_Buffer_Finalize.restype = c_size_t | |
#/** @brief Wrapper for MediaInfoLib::MediaInfo::Save */ | |
#MEDIAINFO_EXP size_t __stdcall MediaInfo_Save (void* Handle); | |
MediaInfo_Save = MediaInfoDLL_Handler.MediaInfo_Save | |
MediaInfo_Save.argtypes = [c_void_p] | |
MediaInfo_Save.restype = c_size_t | |
#/** @brief Wrapper for MediaInfoLib::MediaInfo::Close */ | |
#MEDIAINFO_EXP void __stdcall MediaInfo_Close (void* Handle); | |
MediaInfo_Close = MediaInfoDLL_Handler.MediaInfo_Close | |
MediaInfo_Close.argtypes = [c_void_p] | |
MediaInfo_Close.restype = None | |
#/** @brief Wrapper for MediaInfoLib::MediaInfo::Inform */ | |
#MEDIAINFO_EXP const wchar_t* __stdcall MediaInfo_Inform (void* Handle, size_t Reserved); /*Default : Reserved=0*/ | |
MediaInfo_Inform = MediaInfoDLL_Handler.MediaInfo_Inform | |
MediaInfo_Inform.argtypes = [c_void_p, c_size_t] | |
MediaInfo_Inform.restype = c_wchar_p | |
MediaInfoA_Inform = MediaInfoDLL_Handler.MediaInfoA_Inform | |
MediaInfoA_Inform.argtypes = [c_void_p, c_size_t] | |
MediaInfoA_Inform.restype = c_char_p | |
#/** @brief Wrapper for MediaInfoLib::MediaInfo::Get */ | |
#MEDIAINFO_EXP const wchar_t* __stdcall MediaInfo_GetI (void* Handle, MediaInfo_stream_C StreamKind, size_t StreamNumber, size_t Parameter, MediaInfo_info_C InfoKind); /*Default : InfoKind=Info_Text*/ | |
MediaInfo_GetI = MediaInfoDLL_Handler.MediaInfo_GetI | |
MediaInfo_GetI.argtypes = [c_void_p, c_size_t, c_size_t, c_size_t, c_size_t] | |
MediaInfo_GetI.restype = c_wchar_p | |
MediaInfoA_GetI = MediaInfoDLL_Handler.MediaInfoA_GetI | |
MediaInfoA_GetI.argtypes = [c_void_p, c_size_t, c_size_t, c_size_t, c_size_t] | |
MediaInfoA_GetI.restype = c_char_p | |
#/** @brief Wrapper for MediaInfoLib::MediaInfo::Get */ | |
#MEDIAINFO_EXP const wchar_t* __stdcall MediaInfo_Get (void* Handle, MediaInfo_stream_C StreamKind, size_t StreamNumber, const wchar_t* Parameter, MediaInfo_info_C InfoKind, MediaInfo_info_C SearchKind); /*Default : InfoKind=Info_Text, SearchKind=Info_Name*/ | |
MediaInfo_Get = MediaInfoDLL_Handler.MediaInfo_Get | |
MediaInfo_Get.argtypes = [c_void_p, c_size_t, c_size_t, c_wchar_p, c_size_t, c_size_t] | |
MediaInfo_Get.restype = c_wchar_p | |
MediaInfoA_Get = MediaInfoDLL_Handler.MediaInfoA_Get | |
MediaInfoA_Get.argtypes = [c_void_p, c_size_t, c_size_t, c_wchar_p, c_size_t, c_size_t] | |
MediaInfoA_Get.restype = c_char_p | |
#/** @brief Wrapper for MediaInfoLib::MediaInfo::Set */ | |
#MEDIAINFO_EXP size_t __stdcall MediaInfo_SetI (void* Handle, const wchar_t* ToSet, MediaInfo_stream_C StreamKind, size_t StreamNumber, size_t Parameter, const wchar_t* OldParameter); | |
MediaInfo_SetI = MediaInfoDLL_Handler.MediaInfo_SetI | |
MediaInfo_SetI.argtypes = [c_void_p, c_wchar_p, c_size_t, c_size_t, c_size_t, c_wchar_p] | |
MediaInfo_SetI.restype = c_void_p | |
MediaInfoA_SetI = MediaInfoDLL_Handler.MediaInfoA_SetI | |
MediaInfoA_SetI.argtypes = [c_void_p, c_char_p, c_size_t, c_size_t, c_size_t, c_wchar_p] | |
MediaInfoA_SetI.restype = c_void_p | |
#/** @brief Wrapper for MediaInfoLib::MediaInfo::Set */ | |
#MEDIAINFO_EXP size_t __stdcall MediaInfo_Set (void* Handle, const wchar_t* ToSet, MediaInfo_stream_C StreamKind, size_t StreamNumber, const wchar_t* Parameter, const wchar_t* OldParameter); | |
MediaInfo_Set = MediaInfoDLL_Handler.MediaInfo_Set | |
MediaInfo_Set.argtypes = [c_void_p, c_wchar_p, c_size_t, c_size_t, c_wchar_p, c_wchar_p] | |
MediaInfo_Set.restype = c_size_t | |
MediaInfoA_Set = MediaInfoDLL_Handler.MediaInfoA_Set | |
MediaInfoA_Set.argtypes = [c_void_p, c_char_p, c_size_t, c_size_t, c_wchar_p, c_wchar_p] | |
MediaInfoA_Set.restype = c_size_t | |
#/** @brief Wrapper for MediaInfoLib::MediaInfo::Option */ | |
#MEDIAINFO_EXP const wchar_t* __stdcall MediaInfo_Option (void* Handle, const wchar_t* Option, const wchar_t* Value); | |
MediaInfo_Option = MediaInfoDLL_Handler.MediaInfo_Option | |
MediaInfo_Option.argtypes = [c_void_p, c_wchar_p, c_wchar_p] | |
MediaInfo_Option.restype = c_wchar_p | |
MediaInfoA_Option = MediaInfoDLL_Handler.MediaInfoA_Option | |
MediaInfoA_Option.argtypes = [c_void_p, c_char_p, c_char_p] | |
MediaInfoA_Option.restype = c_char_p | |
#/** @brief Wrapper for MediaInfoLib::MediaInfo::State_Get */ | |
#MEDIAINFO_EXP size_t __stdcall MediaInfo_State_Get (void* Handle); | |
MediaInfo_State_Get = MediaInfoDLL_Handler.MediaInfo_State_Get | |
MediaInfo_State_Get.argtypes = [c_void_p] | |
MediaInfo_State_Get.restype = c_size_t | |
#/** @brief Wrapper for MediaInfoLib::MediaInfo::Count_Get */ | |
#MEDIAINFO_EXP size_t __stdcall MediaInfo_Count_Get (void* Handle, MediaInfo_stream_C StreamKind, size_t StreamNumber); /*Default : StreamNumber=-1*/ | |
MediaInfo_Count_Get = MediaInfoDLL_Handler.MediaInfo_Count_Get | |
MediaInfo_Count_Get.argtypes = [c_void_p, c_size_t, c_size_t] | |
MediaInfo_Count_Get.restype = c_size_t | |
Handle = c_void_p(0) | |
MustUseAnsi = 0 | |
#Handling | |
def __init__(self): | |
self.Handle=self.MediaInfo_New() | |
self.MediaInfo_Option(self.Handle, "CharSet", "UTF-8") | |
def __del__(self): | |
self.MediaInfo_Delete(self.Handle) | |
def Open(self, File): | |
if MustUseAnsi: | |
return self.MediaInfoA_Open (self.Handle, File.encode("utf-8")); | |
else: | |
return self.MediaInfo_Open (self.Handle, File); | |
def Open_Buffer(self, Begin, Begin_Size, End=None, End_Size=0): | |
return self.MediaInfo_Open_Buffer(self.Handle, Begin, Begin_Size, End, End_Size) | |
def Open_Buffer_Init(self, File_Size, File_Offset=0): | |
return self.MediaInfo_Open_Buffer_Init(self.Handle, File_Size, File_Offset) | |
def Open_Buffer_Continue(self, Buffer, Buffer_Size): | |
return self.MediaInfo_Open_Buffer_Continue(self.Handle, Buffer, Buffer_Size) | |
def Open_Buffer_Continue_GoTo_Get(self): | |
return self.MediaInfo_Open_Buffer_Continue_GoTo_Get(self.Handle) | |
def Open_Buffer_Finalize(self): | |
return self.MediaInfo_Open_Buffer_Finalize(self.Handle) | |
def Save(self): | |
return self.MediaInfo_Save(self.Handle) | |
def Close(self): | |
return self.MediaInfo_Close(self.Handle) | |
#General information | |
def Inform(self): | |
if MustUseAnsi: | |
return self.MediaInfoA_Inform(self.Handle, 0).decode("utf_8", 'ignore') | |
else: | |
return self.MediaInfo_Inform(self.Handle, 0) | |
def Get(self, StreamKind, StreamNumber, Parameter, InfoKind=Info.Text, SearchKind=Info.Name): | |
if MustUseAnsi: | |
return self.MediaInfoA_Get(self.Handle, StreamKind, StreamNumber, Parameter.encode("utf-8"), InfoKind, SearchKind).decode("utf_8", 'ignore') | |
else: | |
return self.MediaInfo_Get(self.Handle, StreamKind, StreamNumber, Parameter, InfoKind, SearchKind) | |
def GetI(self, StreamKind, StreamNumber, Parameter, InfoKind=Info.Text): | |
if MustUseAnsi: | |
return self.MediaInfoA_GetI(self.Handle, StreamKind, StreamNumber, Parameter, InfoKind).decode("utf_8", 'ignore') | |
else: | |
return self.MediaInfo_GetI(self.Handle, StreamKind, StreamNumber, Parameter, InfoKind) | |
def Set(self, ToSet, StreamKind, StreamNumber, Parameter, OldParameter=""): | |
if MustUseAnsi: | |
return self.MediaInfoA_Set(self.Handle, ToSet, StreamKind, StreamNumber, Parameter.encode("utf-8"), OldParameter.encode("utf-8")) | |
else: | |
return self.MediaInfo_Set(self.Handle, ToSet, StreamKind, StreamNumber, Parameter, OldParameter) | |
def SetI(self, ToSet, StreamKind, StreamNumber, Parameter, OldValue): | |
if MustUseAnsi: | |
return self.MediaInfoA_SetI(self.Handle, ToSet, StreamKind, StreamNumber, Parameter, OldValue.encode("utf-8")) | |
else: | |
return self.MediaInfo_SetI(self.Handle, ToSet, StreamKind, StreamNumber, Parameter, OldValue) | |
#Options | |
def Option(self, Option, Value=""): | |
if MustUseAnsi: | |
return self.MediaInfoA_Option(self.Handle, Option.encode("utf-8"), Value.encode("utf-8")).decode("utf_8", 'ignore') | |
else: | |
return self.MediaInfo_Option(self.Handle, Option, Value) | |
def Option_Static(self, Option, Value=""): | |
if MustUseAnsi: | |
return self.MediaInfoA_Option(None, Option.encode("utf-8"), Value.encode("utf-8")).decode("utf_8", 'ignore') | |
else: | |
return self.MediaInfo_Option(None, Option, Value) | |
def State_Get(self): | |
return self.MediaInfo_State_Get(self.Handle) | |
def Count_Get(self, StreamKind, StreamNumber=-1): | |
return self.MediaInfo_Count_Get(self.Handle, StreamKind, StreamNumber) | |
class MediaInfoList: | |
#/** @brief A 'new' MediaInfoList interface, return a Handle, don't forget to delete it after using it*/ | |
#MEDIAINFO_EXP void* __stdcall MediaInfoList_New (); /*you must ALWAYS call MediaInfoList_Delete(Handle) in order to free memory*/ | |
MediaInfoList_New = MediaInfoDLL_Handler.MediaInfoList_New | |
MediaInfoList_New.argtypes = [] | |
MediaInfoList_New.restype = c_void_p | |
#/** @brief A 'new' MediaInfoList interface (with a quick init of useful options : "**VERSION**;**APP_NAME**;**APP_VERSION**", but without debug information, use it only if you know what you do), return a Handle, don't forget to delete it after using it*/ | |
#MEDIAINFO_EXP void* __stdcall MediaInfoList_New_Quick (const wchar_t* Files, const wchar_t* Config); /*you must ALWAYS call MediaInfoList_Delete(Handle) in order to free memory*/ | |
MediaInfoList_New_Quick = MediaInfoDLL_Handler.MediaInfoList_New_Quick | |
MediaInfoList_New_Quick.argtypes = [c_wchar_p, c_wchar_p] | |
MediaInfoList_New_Quick.restype = c_void_p | |
#/** @brief Delete a MediaInfoList interface*/ | |
#MEDIAINFO_EXP void __stdcall MediaInfoList_Delete (void* Handle); | |
MediaInfoList_Delete = MediaInfoDLL_Handler.MediaInfoList_Delete | |
MediaInfoList_Delete.argtypes = [c_void_p] | |
#/** @brief Wrapper for MediaInfoListLib::MediaInfoList::Open (with a filename)*/ | |
#MEDIAINFO_EXP size_t __stdcall MediaInfoList_Open (void* Handle, const wchar_t* Files, const MediaInfo_fileoptions_C Options); /*Default : Options=MediaInfo_FileOption_Nothing*/ | |
MediaInfoList_Open = MediaInfoDLL_Handler.MediaInfoList_Open | |
MediaInfoList_Open.argtypes = [c_void_p, c_wchar_p, c_void_p] | |
MediaInfoList_Open.restype = c_void_p | |
#/** @brief Wrapper for MediaInfoListLib::MediaInfoList::Open (with a buffer) */ | |
#MEDIAINFO_EXP size_t __stdcall MediaInfoList_Open_Buffer (void* Handle, const unsigned char* Begin, size_t Begin_Size, const unsigned char* End, size_t End_Size); /*return Handle*/ | |
MediaInfoList_Open_Buffer = MediaInfoDLL_Handler.MediaInfoList_Open_Buffer | |
MediaInfoList_Open_Buffer.argtypes = [c_void_p, c_void_p, c_void_p, c_void_p, c_void_p] | |
MediaInfoList_Open_Buffer.restype = c_void_p | |
#/** @brief Wrapper for MediaInfoListLib::MediaInfoList::Save */ | |
#MEDIAINFO_EXP size_t __stdcall MediaInfoList_Save (void* Handle, size_t FilePos); | |
MediaInfoList_Save = MediaInfoDLL_Handler.MediaInfoList_Save | |
MediaInfoList_Save.argtypes = [c_void_p, c_void_p] | |
MediaInfoList_Save.restype = c_void_p | |
#/** @brief Wrapper for MediaInfoListLib::MediaInfoList::Close */ | |
#MEDIAINFO_EXP void __stdcall MediaInfoList_Close (void* Handle, size_t FilePos); | |
MediaInfoList_Close = MediaInfoDLL_Handler.MediaInfoList_Close | |
MediaInfoList_Close.argtypes = [c_void_p, c_void_p] | |
#/** @brief Wrapper for MediaInfoListLib::MediaInfoList::Inform */ | |
#MEDIAINFO_EXP const wchar_t* __stdcall MediaInfoList_Inform (void* Handle, size_t FilePos, size_t Reserved); /*Default : Reserved=0*/ | |
MediaInfoList_Inform = MediaInfoDLL_Handler.MediaInfoList_Inform | |
MediaInfoList_Inform.argtypes = [c_void_p, c_void_p, c_void_p] | |
MediaInfoList_Inform.restype = c_wchar_p | |
#/** @brief Wrapper for MediaInfoListLib::MediaInfoList::Get */ | |
#MEDIAINFO_EXP const wchar_t* __stdcall MediaInfoList_GetI (void* Handle, size_t FilePos, MediaInfo_stream_C StreamKind, size_t StreamNumber, size_t Parameter, MediaInfo_info_C InfoKind); /*Default : InfoKind=Info_Text*/ | |
MediaInfoList_GetI = MediaInfoDLL_Handler.MediaInfoList_GetI | |
MediaInfoList_GetI.argtypes = [c_void_p, c_void_p, c_void_p, c_void_p, c_void_p, c_void_p] | |
MediaInfoList_GetI.restype = c_wchar_p | |
#/** @brief Wrapper for MediaInfoListLib::MediaInfoList::Get */ | |
#MEDIAINFO_EXP const wchar_t* __stdcall MediaInfoList_Get (void* Handle, size_t FilePos, MediaInfo_stream_C StreamKind, size_t StreamNumber, const wchar_t* Parameter, MediaInfo_info_C InfoKind, MediaInfo_info_C SearchKind); /*Default : InfoKind=Info_Text, SearchKind=Info_Name*/ | |
MediaInfoList_Get = MediaInfoDLL_Handler.MediaInfoList_Get | |
MediaInfoList_Get.argtypes = [c_void_p, c_void_p, c_void_p, c_void_p, c_wchar_p, c_void_p, c_void_p] | |
MediaInfoList_Get.restype = c_wchar_p | |
#/** @brief Wrapper for MediaInfoListLib::MediaInfoList::Set */ | |
#MEDIAINFO_EXP size_t __stdcall MediaInfoList_SetI (void* Handle, const wchar_t* ToSet, size_t FilePos, MediaInfo_stream_C StreamKind, size_t StreamNumber, size_t Parameter, const wchar_t* OldParameter); | |
MediaInfoList_SetI = MediaInfoDLL_Handler.MediaInfoList_SetI | |
MediaInfoList_SetI.argtypes = [c_void_p, c_wchar_p, c_void_p, c_void_p, c_void_p, c_void_p, c_wchar_p] | |
MediaInfoList_SetI.restype = c_void_p | |
#/** @brief Wrapper for MediaInfoListLib::MediaInfoList::Set */ | |
#MEDIAINFO_EXP size_t __stdcall MediaInfoList_Set (void* Handle, const wchar_t* ToSet, size_t FilePos, MediaInfo_stream_C StreamKind, size_t StreamNumber, const wchar_t* Parameter, const wchar_t* OldParameter); | |
MediaInfoList_Set = MediaInfoDLL_Handler.MediaInfoList_Set | |
MediaInfoList_Set.argtypes = [c_void_p, c_wchar_p, c_void_p, c_void_p, c_void_p, c_wchar_p, c_wchar_p] | |
MediaInfoList_Set.restype = c_void_p | |
#/** @brief Wrapper for MediaInfoListLib::MediaInfoList::Option */ | |
#MEDIAINFO_EXP const wchar_t* __stdcall MediaInfoList_Option (void* Handle, const wchar_t* Option, const wchar_t* Value); | |
MediaInfoList_Option = MediaInfoDLL_Handler.MediaInfoList_Option | |
MediaInfoList_Option.argtypes = [c_void_p, c_wchar_p, c_wchar_p] | |
MediaInfoList_Option.restype = c_wchar_p | |
#/** @brief Wrapper for MediaInfoListLib::MediaInfoList::State_Get */ | |
#MEDIAINFO_EXP size_t __stdcall MediaInfoList_State_Get (void* Handle); | |
MediaInfoList_State_Get = MediaInfoDLL_Handler.MediaInfoList_State_Get | |
MediaInfoList_State_Get.argtypes = [c_void_p] | |
MediaInfoList_State_Get.restype = c_void_p | |
#/** @brief Wrapper for MediaInfoListLib::MediaInfoList::Count_Get */ | |
#MEDIAINFO_EXP size_t __stdcall MediaInfoList_Count_Get (void* Handle, size_t FilePos, MediaInfo_stream_C StreamKind, size_t StreamNumber); /*Default : StreamNumber=-1*/ | |
MediaInfoList_Count_Get = MediaInfoDLL_Handler.MediaInfoList_Count_Get | |
MediaInfoList_Count_Get.argtypes = [c_void_p, c_void_p, c_void_p, c_void_p] | |
MediaInfoList_Count_Get.restype = c_void_p | |
#/** @brief Wrapper for MediaInfoListLib::MediaInfoList::Count_Get */ | |
#MEDIAINFO_EXP size_t __stdcall MediaInfoList_Count_Get_Files (void* Handle); | |
MediaInfoList_Count_Get_Files = MediaInfoDLL_Handler.MediaInfoList_Count_Get_Files | |
MediaInfoList_Count_Get_Files.argtypes = [c_void_p] | |
MediaInfoList_Count_Get_Files.restype = c_void_p | |
Handle = c_void_p(0) | |
#Handling | |
def __init__(self): | |
self.Handle=MediaInfoList_New() | |
def __del__(self): | |
MediaInfoList_Delete(self.Handle) | |
def Open(self, Files, Options=FileOptions.Nothing): | |
return MediaInfoList_Open(self.Handle, Files, Options) | |
def Open_Buffer(self, Begin, Begin_Size, End=None, End_Size=0): | |
return MediaInfoList_Open_Buffer (self.Handle, Begin, Begin_Size, End, End_Size) | |
def Save(self, FilePos): | |
return MediaInfoList_Save(self.Handle, FilePos) | |
def Close(self, FilePos): | |
MediaInfoList_Close (self.Handle, FilePos) | |
#General information | |
def Inform(self, FilePos, Reserved=0): | |
return MediaInfoList_Inform (self.Handle, FilePos, Reserved) | |
def GetI(self, FilePos, StreamKind, StreamNumber, Parameter, InfoKind=Info.Text): | |
return MediaInfoList_GetI (self.Handle, FilePos, StreamKind, StreamNumber, Parameter, InfoKind) | |
def Get(self, FilePos, StreamKind, StreamNumber, Parameter, InfoKind=Info.Text, SearchKind=Info.Name): | |
return MediaInfoList_Get (self.Handle, FilePos, StreamKind, StreamNumber, (Parameter), InfoKind, SearchKind) | |
def SetI(self, ToSet, FilePos, StreamKind, StreamNumber, Parameter, OldParameter=""): | |
return MediaInfoList_SetI (self, Handle, ToSet, FilePos, StreamKind, StreamNumber, Parameter, OldParameter) | |
def Set(self, ToSet, FilePos, StreamKind, StreamNumber, Parameter, OldParameter=""): | |
return MediaInfoList_Set (self.Handle, ToSet, FilePos, StreamKind, StreamNumber, Parameter, OldParameter) | |
#Options | |
def Option(self, Option, Value=""): | |
return MediaInfoList_Option (self.Handle, Option, Value) | |
def Option_Static(self, Option, Value=""): | |
return MediaInfoList_Option(None, Option, Value) | |
def State_Get(self): | |
return MediaInfoList_State_Get (self.Handle) | |
def Count_Get(self, FilePos, StreamKind, StreamNumber): | |
return MediaInfoList_Count_Get (self.Handle, FilePos, StreamKind, StreamNumber=-1) | |
def Count_Get_Files(self): | |
return MediaInfoList_Count_Get_Files (self.Handle) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import re, os, sys | |
from MediaInfoDLL3 import * | |
def MediaInput(): | |
Input=sys.argv[1] | |
return(Input) | |
MediaRead = MediaInput() | |
MI = MediaInfo() | |
############################ | |
#Function Call For Template | |
############################ | |
def VideoFormatCodec(): | |
MI.Open(MediaRead) | |
MI.Option_Static("Inform", "Video;%Format%") | |
Codec=MI.Inform() | |
MI.Close() | |
return(Codec) | |
def VideoEncoder(): | |
MI.Open(MediaRead) | |
MI.Option_Static("Inform", "Video;%Format%") | |
Encoder=MI.Inform() | |
if Encoder == "AVC": | |
EncoderFile="x264" | |
MI.Close() | |
return(EncoderFile) | |
elif Encoder == "HEVC": | |
EncoderFile="x265" | |
MI.Close() | |
return(EncoderFile) | |
def VideoContainer(): | |
MI.Open(MediaRead) | |
MI.Option_Static("Inform", "General;%FileExtension%") | |
VideoContainerFile=MI.Inform() | |
MI.Close() | |
return(VideoContainerFile) | |
def VideoLength(): | |
MI.Open(MediaRead) | |
MI.Option_Static("Inform", "General;%Duration_String4%") | |
DurationFile=MI.Inform() | |
MI.Close() | |
return(DurationFile) | |
def VideoLengthMod(): | |
MI.Open(MediaRead) | |
MI.Option_Static("Inform", "General;%Duration_String5%") | |
DurationFile=MI.Inform() | |
MI.Close() | |
regex = r"(.*)\:(.*?)\:(.*)\..*" | |
matches = re.search(regex, DurationFile, re.DOTALL) | |
if matches: | |
Hour=matches.group(1) | |
Min=matches.group(2) | |
Second=matches.group(3) | |
Build=Hour+":"+Min+":"+Second | |
return(Build) | |
def VideoLength_Norm(): | |
MI.Open(MediaRead) | |
MI.Option_Static("Inform", "General;%Duration_String2%") | |
DurationFile=MI.Inform() | |
MI.Close() | |
return(DurationFile) | |
def ChromaSubsampling(): | |
MI.Open(MediaRead) | |
MI.Option_Static("Inform", "Video;%ChromaSubsampling_String%") | |
Chroma=MI.Inform() | |
ExportChroma=Chroma.replace(":", "") | |
MI.Close() | |
return(ExportChroma) | |
def Matrix(): | |
MI.Open(MediaRead) | |
MI.Option_Static("Inform", "Video;%matrix_coefficients%") | |
BT=MI.Inform().lower() | |
MI.Close() | |
return(BT) | |
def VideoRange(): | |
MI.Open(MediaRead) | |
MI.Option_Static("Inform", "Video;%colour_range%") | |
Range=MI.Inform() | |
if Range == "Limited": | |
MI.Close() | |
return("tv") | |
elif Range == "": | |
return("Unknown") | |
else: | |
MI.Close() | |
return("pc") | |
def VideoBitRate(): | |
MI.Open(MediaRead) | |
MI.Option_Static("Inform", "General;%OverallBitRate_String%") | |
VideoBiteRate=MI.Inform() | |
MI.Close() | |
return(VideoBiteRate) | |
def VideoProfile(): | |
MI.Open(MediaRead) | |
MI.Option_Static("Inform", "Video;%Format_Profile%") | |
VProfile=MI.Inform()[:4] | |
MI.Close() | |
return(VProfile) | |
def VVideoBitRate(): | |
MI.Open(MediaRead) | |
MI.Option_Static("Inform", "Video;%BitRate_String%") | |
VideoBiteRateMaxs=MI.Inform() | |
MI.Close() | |
return(VideoBiteRateMaxs) | |
def VideoBitRateMod(): | |
MI.Open(MediaRead) | |
MI.Option_Static("Inform", "General;%OverallBitRate_String%") | |
VideoBiteRate=MI.Inform() | |
Rate=VideoBiteRate[:5] | |
MI.Close() | |
return(Rate) | |
def VideoHeight(): | |
MI.Open(MediaRead) | |
MI.Option_Static("Inform", "Video;%Height%") | |
Height=MI.Inform() | |
MI.Close() | |
return(Height) | |
def VideoWidth(): | |
MI.Open(MediaRead) | |
MI.Option_Static("Inform", "Video;%Width%") | |
Width=MI.Inform() | |
MI.Close() | |
return(Width) | |
def VideoFormat(): | |
MI.Open(MediaRead) | |
MI.Option_Static("Inform", "Video;%Height%") | |
Height=MI.Inform() | |
MI.Option_Static("Inform", "Video;%Width%") | |
Width=MI.Inform() | |
if Height == "480": | |
Format = "480p" | |
MI.Close() | |
return(Format) | |
elif Width == "1280" and Height == "720": | |
Format="720p" | |
MI.Close() | |
return(Format) | |
elif Width == "1920" and Height == "1080": | |
Format="1080p" | |
MI.Close() | |
return(Format) | |
elif Width == "2560" and Height == "1440": | |
Format="2k" | |
MI.Close() | |
return(Format) | |
elif Width == "3840" and Height == "2160": | |
Format="4K" | |
MI.Close() | |
return(Format) | |
def VideoFrameRate(): | |
MI.Open(MediaRead) | |
MI.Option_Static("Inform", "General;%FrameRate%") | |
VFrameRates=MI.Inform() | |
MI.Close() | |
return(VFrameRates) | |
def VideoFrameRateFPS(): | |
MI.Open(MediaRead) | |
MI.Option_Static("Inform", "General;%FrameRate_String%") | |
VFrameRatesFPS=MI.Inform().lower() | |
MI.Close() | |
return(VFrameRatesFPS) | |
def FileSize(): | |
MI.Open(MediaRead) | |
MI.Option_Static("Inform", "General;%FileSize_String2%") | |
FileSize=MI.Inform() | |
MI.Close() | |
return(FileSize) | |
def FileSizeBytes(): | |
MI.Open(MediaRead) | |
MI.Option_Static("Inform", "General;%FileSize%") | |
FileSizeByte=MI.Inform() | |
MI.Close() | |
return(FileSizeByte) | |
def ColorSpace(): | |
MI.Open(MediaRead) | |
MI.Option_Static("Inform", "Video;%ColorSpace%") | |
ColorSpaces=MI.Inform().lower() | |
MI.Close() | |
return(ColorSpaces) | |
def BitDepth(): | |
MI.Open(MediaRead) | |
MI.Option_Static("Inform", "Video;%BitDepth%") | |
VideoDepth=MI.Inform() | |
return(VideoDepth) | |
def FileName(): | |
MI.Open(MediaRead) | |
MI.Option_Static("Inform", "General;%CompleteName%") | |
FileNames=MI.Inform() | |
Path=os.path.basename(FileNames) | |
return(Path) | |
def ScanType(): | |
MI.Open(MediaRead) | |
MI.Option_Static("Inform", "Video;%ScanType%") | |
ScanType=MI.Inform() | |
MI.Close() | |
return(ScanType) | |
def AudioCodec(): | |
MI.Open(MediaRead) | |
MI.Option_Static("Inform", "Audio;%Format%") | |
AudioFormat=MI.Inform() | |
MI.Close() | |
return(AudioFormat) | |
def AudioBitRate(): | |
MI.Open(MediaRead) | |
MI.Option_Static("Inform", "Audio;%BitRate_String%") | |
AudioBitRate=MI.Inform() | |
MI.Close() | |
return(AudioBitRate) | |
def AudioSampleRate(): | |
MI.Open(MediaRead) | |
MI.Option_Static("Inform", "Audio;%SamplingRate_String%") | |
AudioSampleRate=MI.Inform() | |
MI.Close() | |
return(AudioSampleRate) | |
def Channels(): | |
MI.Open(MediaRead) | |
MI.Option_Static("Inform", "Audio;%Channel(s)%") | |
Channel=MI.Inform() | |
if Channel == "1": | |
MI.Close() | |
return("Mono") | |
elif Channel == "2": | |
MI.Close() | |
return("Stereo") | |
elif Channel == "5.1": | |
MI.Close() | |
return("Surround Sound") | |
elif Channel == "7.1": | |
MI.Close() | |
return("Surround Sound") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"""Create a video contact sheet. | |
""" | |
from __future__ import print_function | |
import os | |
import subprocess | |
import sys | |
import datetime | |
import Settings | |
try: | |
from subprocess import DEVNULL | |
except ImportError: | |
DEVNULL = open(os.devnull, 'wb') | |
import argparse | |
import json | |
import math | |
import tempfile | |
import textwrap | |
from collections import namedtuple | |
from enum import Enum | |
from glob import glob | |
from PIL import Image, ImageDraw, ImageFont | |
import numpy | |
from jinja2 import Template | |
import texttable | |
import parsedatetime | |
__version__ = "7" | |
__author__ = "Nils Amiet" | |
class Grid(namedtuple('Grid', ['x', 'y'])): | |
def __str__(self): | |
return "%sx%s" % (self.x, self.y) | |
class Frame(namedtuple('Frame', ['filename', 'blurriness', 'timestamp', 'avg_color'])): | |
pass | |
class Color(namedtuple('Color', ['r', 'g', 'b', 'a'])): | |
def to_hex(self, component): | |
h = hex(component).replace("0x", "").upper() | |
return h if len(h) == 2 else "0" + h | |
def __str__(self): | |
return "".join([self.to_hex(x) for x in [self.r, self.g, self.b, self.a]]) | |
TimestampPosition = Enum('TimestampPosition', "north south east west ne nw se sw center") | |
VALID_TIMESTAMP_POSITIONS = [x.name for x in TimestampPosition] | |
DEFAULT_METADATA_FONT_SIZE = 16 | |
DEFAULT_METADATA_FONT = "DejaVuSerif-Bold.ttf" | |
DEFAULT_TIMESTAMP_FONT_SIZE = 14 | |
DEFAULT_TIMESTAMP_FONT = "DejaVuSerif-Bold.ttf" | |
DEFAULT_CONTACT_SHEET_WIDTH = 1132 | |
DEFAULT_DELAY_PERCENT = None | |
DEFAULT_START_DELAY_PERCENT = 7 | |
DEFAULT_END_DELAY_PERCENT = DEFAULT_START_DELAY_PERCENT | |
DEFAULT_GRID_SPACING = None | |
DEFAULT_GRID_HORIZONTAL_SPACING = 0 | |
DEFAULT_GRID_VERTICAL_SPACING = DEFAULT_GRID_HORIZONTAL_SPACING | |
DEFAULT_METADATA_POSITION = "top" | |
DEFAULT_METADATA_FONT_COLOR = "ffffff" | |
DEFAULT_BACKGROUND_COLOR = "000000" | |
DEFAULT_TIMESTAMP_FONT_COLOR = "ffffff" | |
DEFAULT_TIMESTAMP_BACKGROUND_COLOR = "000000aa" | |
DEFAULT_TIMESTAMP_BORDER_COLOR = "000000" | |
DEFAULT_TIMESTAMP_BORDER_SIZE = 1 | |
DEFAULT_ACCURATE_DELAY_SECONDS = 1 | |
DEFAULT_METADATA_MARGIN = 10 | |
DEFAULT_METADATA_HORIZONTAL_MARGIN = DEFAULT_METADATA_MARGIN | |
DEFAULT_METADATA_VERTICAL_MARGIN = DEFAULT_METADATA_MARGIN | |
DEFAULT_CAPTURE_ALPHA = 255 | |
DEFAULT_GRID_SIZE = Grid(4, 6) | |
DEFAULT_TIMESTAMP_HORIZONTAL_PADDING = 3 | |
DEFAULT_TIMESTAMP_VERTICAL_PADDING = 1 | |
DEFAULT_TIMESTAMP_HORIZONTAL_MARGIN = 5 | |
DEFAULT_TIMESTAMP_VERTICAL_MARGIN = 5 | |
DEFAULT_IMAGE_QUALITY = 95 | |
DEFAULT_IMAGE_FORMAT = "jpg" | |
DEFAULT_TIMESTAMP_POSITION = TimestampPosition.se | |
DEFAULT_FRAME_TYPE = None | |
DEFAULT_INTERVAL = None | |
class MediaInfo(object): | |
"""Collect information about a video file | |
""" | |
def __init__(self, path, verbose=False): | |
self.probe_media(path) | |
self.find_video_stream() | |
self.find_audio_stream() | |
self.compute_display_resolution() | |
self.compute_format() | |
self.parse_attributes() | |
if verbose: | |
print(self.filename) | |
print("%sx%s" % (self.sample_width, self.sample_height)) | |
print("%sx%s" % (self.display_width, self.display_height)) | |
print(self.duration) | |
print(self.size) | |
def probe_media(self, path): | |
"""Probe video file using ffprobe | |
""" | |
ffprobe_command = [ | |
"ffprobe", | |
"-v", "quiet", | |
"-print_format", "json", | |
"-show_format", | |
"-show_streams", | |
path | |
] | |
try: | |
output = subprocess.check_output(ffprobe_command) | |
self.ffprobe_dict = json.loads(output.decode("utf-8")) | |
except FileNotFoundError: | |
error = "Could not find 'ffprobe' executable. Please make sure ffmpeg/ffprobe is installed and is in your PATH." | |
error_exit(error) | |
def human_readable_size(self, num, suffix='B'): | |
"""Converts a number of bytes to a human readable format | |
""" | |
for unit in ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi']: | |
if abs(num) < 1024.0: | |
return "%3.1f %s%s" % (num, unit, suffix) | |
num /= 1024.0 | |
return "%.1f %s%s" % (num, 'Yi', suffix) | |
def find_video_stream(self): | |
"""Find the first stream which is a video stream | |
""" | |
for stream in self.ffprobe_dict["streams"]: | |
try: | |
if stream["codec_type"] == "video": | |
self.video_stream = stream | |
break | |
except: | |
pass | |
def find_audio_stream(self): | |
"""Find the first stream which is an audio stream | |
""" | |
for stream in self.ffprobe_dict["streams"]: | |
try: | |
if stream["codec_type"] == "audio": | |
self.audio_stream = stream | |
break | |
except: | |
pass | |
def compute_display_resolution(self): | |
"""Computes the display resolution. | |
Some videos have a sample resolution that differs from the display resolution | |
(non-square pixels), thus the proper display resolution has to be computed. | |
""" | |
self.sample_width = int(self.video_stream["width"]) | |
self.sample_height = int(self.video_stream["height"]) | |
sample_aspect_ratio = "1:1" | |
try: | |
sample_aspect_ratio = self.video_stream["sample_aspect_ratio"] | |
except KeyError: | |
pass | |
if sample_aspect_ratio == "1:1": | |
self.display_width = self.sample_width | |
self.display_height = self.sample_height | |
else: | |
sample_split = sample_aspect_ratio.split(":") | |
sw = int(sample_split[0]) | |
sh = int(sample_split[1]) | |
self.display_width = int(self.sample_width * sw / sh) | |
self.display_height = int(self.sample_height) | |
if self.display_width == 0: | |
self.display_width = self.sample_width | |
if self.display_height == 0: | |
self.display_height = self.sample_height | |
def compute_format(self): | |
"""Compute duration, size and retrieve filename | |
""" | |
format_dict = self.ffprobe_dict["format"] | |
self.duration_seconds = float(format_dict["duration"]) | |
self.duration = MediaInfo.pretty_duration(self.duration_seconds) | |
self.filename = os.path.basename(format_dict["filename"]) | |
self.size_bytes = int(format_dict["size"]) | |
self.size = self.human_readable_size(self.size_bytes) | |
@staticmethod | |
def pretty_to_seconds( | |
pretty_duration): | |
"""Converts pretty printed timestamp to seconds | |
""" | |
millis_split = pretty_duration.split(".") | |
millis = 0 | |
if len(millis_split) == 2: | |
millis = int(millis_split[1]) | |
left = millis_split[0] | |
else: | |
left = pretty_duration | |
left_split = left.split(":") | |
if len(left_split) < 3: | |
hours = 0 | |
minutes = int(left_split[0]) | |
seconds = int(left_split[1]) | |
else: | |
hours = int(left_split[0]) | |
minutes = int(left_split[1]) | |
seconds = int(left_split[2]) | |
result = (millis / 1000.0) + seconds + minutes * 60 + hours * 3600 | |
return result | |
@staticmethod | |
def pretty_duration( | |
seconds, | |
show_centis=False, | |
show_millis=False): | |
"""Converts seconds to a human readable time format | |
""" | |
hours = int(math.floor(seconds / 3600)) | |
remaining_seconds = seconds - 3600 * hours | |
minutes = math.floor(remaining_seconds / 60) | |
remaining_seconds = remaining_seconds - 60 * minutes | |
duration = "" | |
if hours > 0: | |
duration += "%s:" % (int(hours),) | |
duration += "%s:%s" % (str(int(minutes)).zfill(2), str(int(math.floor(remaining_seconds))).zfill(2)) | |
if show_centis or show_millis: | |
coeff = 1000 if show_millis else 100 | |
digits = 3 if show_millis else 2 | |
centis = math.floor((remaining_seconds - math.floor(remaining_seconds)) * coeff) | |
duration += ".%s" % (str(int(centis)).zfill(digits)) | |
return duration | |
def desired_size(self, width=DEFAULT_CONTACT_SHEET_WIDTH): | |
"""Computes the height based on a given width and fixed aspect ratio. | |
Returns (width, height) | |
""" | |
ratio = width / float(self.display_width) | |
desired_height = math.floor(self.display_height * ratio) | |
return (int(width), int(desired_height)) | |
def parse_attributes(self): | |
"""Parse multiple media attributes | |
""" | |
# video | |
try: | |
self.video_codec = self.video_stream["codec_name"].title() | |
except KeyError: | |
self.video_codec = None | |
try: | |
self.Video_bit_rate = self.video_stream["bit_rate"] | |
except KeyError: | |
self.Video_bit_rate = None | |
try: | |
self.Video_bitrate = self.video_stream["bit_rate"][:4] | |
except KeyError: | |
self.Video_bitrate = None | |
try: | |
self.video_profile = self.video_stream["profile"].title() | |
except KeyError: | |
self.video_profile = None | |
try: | |
self.color_space = self.video_stream["color_space"] | |
except KeyError: | |
self.color_space = None | |
try: | |
self.pix_fmt = self.video_stream["pix_fmt"] | |
except KeyError: | |
self.pix_fmt = None | |
try: | |
self.color_range = self.video_stream["color_range"] | |
except KeyError: | |
self.color_range = None | |
try: | |
self.nb_frames = self.video_stream["nb_frames"] | |
except KeyError: | |
self.nb_frames = None | |
try: | |
self.video_codec_long = self.video_stream["codec_long_name"] | |
except KeyError: | |
self.video_codec_long = None | |
try: | |
self.sample_aspect_ratio = self.video_stream["sample_aspect_ratio"] | |
except KeyError: | |
self.sample_aspect_ratio = None | |
try: | |
self.bits_per_raw_sample = self.video_stream["bits_per_raw_sample"] | |
except KeyError: | |
self.bits_per_raw_sample = None | |
try: | |
self.display_aspect_ratio = self.video_stream["display_aspect_ratio"] | |
except KeyError: | |
self.display_aspect_ratio = None | |
try: | |
self.frame_rate = self.video_stream["avg_frame_rate"] | |
splits = self.frame_rate.split("/") | |
if len(splits) == 2: | |
self.frame_rate = int(splits[0]) / int(splits[1]) | |
else: | |
self.frame_rate = int(self.frame_rate) | |
self.frame_rate = round(self.frame_rate, 3) | |
except KeyError: | |
self.frame_rate = None | |
except ZeroDivisionError: | |
self.frame_rate = None | |
# audio | |
try: | |
self.audio_codec = self.audio_stream["codec_name"].upper() | |
except (KeyError, AttributeError): | |
self.audio_codec = None | |
try: | |
self.audio_profile = self.audio_stream["profile"] | |
except (KeyError, AttributeError): | |
self.audio_profile = None | |
try: | |
self.audio_codec_long = self.audio_stream["codec_long_name"] | |
except (KeyError, AttributeError): | |
self.audio_codec_long = None | |
try: | |
self.audio_sample_rate = int(self.audio_stream["sample_rate"]) | |
except (KeyError, AttributeError): | |
self.audio_sample_rate = None | |
try: | |
self.audio_sample_rate_string = int(self.audio_stream["sample_rate"][:2]) | |
except (KeyError, AttributeError): | |
self.audio_sample_rate_string = None | |
try: | |
self.audio_channel_layout = str(self.audio_stream["channel_layout"].title()) | |
except (KeyError, AttributeError): | |
self.audio_channel_layout = None | |
try: | |
self.audio_bit_rate = int(self.audio_stream["bit_rate"]) | |
except (KeyError, AttributeError): | |
self.audio_bit_rate = None | |
try: | |
self.audio_bit_rate_string = int(self.audio_stream["bit_rate"][:2]) | |
except (KeyError, AttributeError): | |
self.audio_bit_rate_string = None | |
def template_attributes(self): | |
"""Returns the template attributes and values ready for use in the metadata header | |
""" | |
return dict((x["name"], getattr(self, x["name"])) for x in MediaInfo.list_template_attributes()) | |
@staticmethod | |
def list_template_attributes(): | |
"""Returns a list a of all supported template attributes with their description and example | |
""" | |
table = [] | |
table.append({"name": "size", "description": "File Size in MiB/GiB", "example": "128.3 MiB"}) | |
table.append({"name": "size_bytes", "description": "File Size in bytes", "example": "4662788373"}) | |
table.append({"name": "filename", "description": "File Name", "example": "video.mkv"}) | |
table.append({"name": "video_profile", "description": "video_profile", "example": "Main"}) | |
table.append({"name": "pix_fmt", "description": "Pixel Format", "example": "yuv420p"}) | |
table.append({"name": "color_range", "description": "Color Range", "example": "tv"}) | |
table.append({"name": "color_space", "description": "Color Space", "example": "bt.709"}) | |
table.append({"name": "duration", "description": "Duration", "example": "03:07"}) | |
table.append({"name": "sample_width", "description": "Sample width (pixels)", "example": "1920"}) | |
table.append({"name": "sample_height", "description": "Sample height (pixels)", "example": "1080"}) | |
table.append({"name": "display_width", "description": "Display width (pixels)", "example": "1920"}) | |
table.append({"name": "display_height", "description": "Display height (pixels)", "example": "1080"}) | |
table.append({"name": "video_codec", "description": "Video Codec", "example": "H265"}) | |
table.append({"name": "Video_bit_rate", "description": "Video BitRate in Bytes", "example": "1496344"}) | |
table.append({"name": "bits_per_raw_sample", "description": "Video BitDepth", "example": "8"}) | |
table.append({"name": "Video_bitrate", "description": "Video BitRate in kbs", "example": "1500"}) | |
table.append({"name": "nb_frames", "description": "Number of Frames", "example": "1274"}) | |
table.append({"name": "video_codec_long", "description": "Video codec (long name)", "example": "H.264 / AVC / MPEG-4 AVC "}) | |
table.append({"name": "display_aspect_ratio", "description": "Display aspect ratio", "example": "16:9"}) | |
table.append({"name": "sample_aspect_ratio", "description": "Sample aspect ratio", "example": "1:1"}) | |
table.append({"name": "audio_codec", "description": "Audio codec", "example": "AAC"}) | |
table.append({"name": "audio_codec_long", "description": "Audio codec (long name)", "example": "AAC (Advanced Audio Coding)"}) | |
table.append({"name": "audio_profile", "description": "Audio Profile", "example": "LC"}) | |
table.append({"name": "audio_channel_layout", "description": "audio channel layout", "example": "Surround Sound"}) | |
table.append({"name": "audio_sample_rate", "description": "Audio sample rate (kHz)", "example": "44100"}) | |
table.append({"name": "audio_sample_rate_string", "description": "Audio sample rate String (kHz)", "example": "32"}) | |
table.append({"name": "audio_bit_rate", "description": "Audio bit rate (bits/s)", "example": "192000"}) | |
table.append({"name": "audio_bit_rate_string", "description": "Audio bit rate (bits/s)", "example": "19"}) | |
table.append({"name": "frame_rate", "description": "Frame Per Second", "example": "23.976"}) | |
return table | |
class MediaCapture(object): | |
"""Capture frames of a video | |
""" | |
def __init__(self, path, accurate=False, skip_delay_seconds=DEFAULT_ACCURATE_DELAY_SECONDS, frame_type=DEFAULT_FRAME_TYPE): | |
self.path = path | |
self.accurate = accurate | |
self.skip_delay_seconds = skip_delay_seconds | |
self.frame_type = frame_type | |
def make_capture(self, time, width, height, out_path="out.png"): | |
"""Capture a frame at given time with given width and height using ffmpeg | |
""" | |
skip_delay = MediaInfo.pretty_duration(self.skip_delay_seconds, show_millis=True) | |
ffmpeg_command = [ | |
"ffmpeg", | |
"-ss", time, | |
"-i", self.path, | |
"-vframes", "1", | |
"-s", "%sx%s" % (width, height), | |
] | |
if self.frame_type is not None: | |
select_args = [ | |
"-vf", "select='eq(frame_type\\," + self.frame_type + ")'" | |
] | |
if self.frame_type == "key": | |
select_args = [ | |
"-vf", "select=key" | |
] | |
if self.frame_type is not None: | |
ffmpeg_command += select_args | |
ffmpeg_command += [ | |
"-y", | |
out_path | |
] | |
if self.accurate: | |
time_seconds = MediaInfo.pretty_to_seconds(time) | |
skip_time_seconds = time_seconds - self.skip_delay_seconds | |
if skip_time_seconds < 0: | |
ffmpeg_command = [ | |
"ffmpeg", | |
"-i", self.path, | |
"-ss", time, | |
"-vframes", "1", | |
"-s", "%sx%s" % (width, height), | |
] | |
if self.frame_type is not None: | |
ffmpeg_command += select_args | |
ffmpeg_command += [ | |
"-y", | |
out_path | |
] | |
else: | |
skip_time = MediaInfo.pretty_duration(skip_time_seconds, show_millis=True) | |
ffmpeg_command = [ | |
"ffmpeg", | |
"-ss", skip_time, | |
"-i", self.path, | |
"-ss", skip_delay, | |
"-vframes", "1", | |
"-s", "%sx%s" % (width, height), | |
] | |
if self.frame_type is not None: | |
ffmpeg_command += select_args | |
ffmpeg_command += [ | |
"-y", | |
out_path | |
] | |
try: | |
subprocess.call(ffmpeg_command, stderr=DEVNULL, stdout=DEVNULL) | |
except FileNotFoundError: | |
error = "Could not find 'ffmpeg' executable. Please make sure ffmpeg/ffprobe is installed and is in your PATH." | |
error_exit(error) | |
def compute_avg_color(self, image_path): | |
"""Computes the average color of an image | |
""" | |
i = Image.open(image_path) | |
i = i.convert('P') | |
p = i.getcolors() | |
# compute avg color | |
total_count = 0 | |
avg_color = 0 | |
for count, color in p: | |
total_count += count | |
avg_color += count * color | |
avg_color /= total_count | |
return avg_color | |
def compute_blurriness(self, image_path): | |
"""Computes the blurriness of an image. Small value means less blurry. | |
""" | |
i = Image.open(image_path) | |
i = i.convert('L') # convert to grayscale | |
a = numpy.asarray(i) | |
b = abs(numpy.fft.rfft2(a)) | |
max_freq = self.avg9x(b) | |
if max_freq is not 0: | |
return 1 | |
else: | |
return 1 | |
def avg9x(self, matrix, percentage=0.05): | |
"""Computes the median of the top n% highest values. | |
By default, takes the top 5% | |
""" | |
xs = matrix.flatten() | |
srt = sorted(xs, reverse=True) | |
length = int(math.floor(percentage * len(srt))) | |
matrix_subset = srt[:length] | |
return numpy.median(matrix_subset) | |
def max_freq(self, matrix): | |
"""Returns the maximum value in the matrix | |
""" | |
m = 0 | |
for row in matrix: | |
mx = max(row) | |
if mx > m: | |
m = mx | |
return m | |
def grid_desired_size( | |
grid, | |
media_info, | |
width=DEFAULT_CONTACT_SHEET_WIDTH, | |
horizontal_margin=DEFAULT_GRID_HORIZONTAL_SPACING): | |
"""Computes the size of the images placed on a mxn grid with given fixed width. | |
Returns (width, height) | |
""" | |
if grid: | |
desired_width = (width - (grid.x - 1) * horizontal_margin) / grid.x | |
else: | |
desired_width = width | |
return media_info.desired_size(width=desired_width) | |
def total_delay_seconds(media_info, args): | |
"""Computes the total seconds to skip (beginning + ending). | |
""" | |
start_delay_seconds = math.floor(media_info.duration_seconds * args.start_delay_percent / 100) | |
end_delay_seconds = math.floor(media_info.duration_seconds * args.end_delay_percent / 100) | |
delay = start_delay_seconds + end_delay_seconds | |
return delay | |
def timestamp_generator(media_info, args): | |
"""Generates `num_samples` uniformly distributed timestamps over time. | |
Timestamps will be selected in the range specified by start_delay_percent and end_delay percent. | |
For example, `end_delay_percent` can be used to avoid making captures during the ending credits. | |
""" | |
delay = total_delay_seconds(media_info, args) | |
capture_interval = (media_info.duration_seconds - delay) / (args.num_samples + 1) | |
if args.interval is not None: | |
capture_interval = int(args.interval.total_seconds()) | |
start_delay_seconds = math.floor(media_info.duration_seconds * args.start_delay_percent / 100) | |
time = start_delay_seconds + capture_interval | |
for i in range(args.num_samples): | |
yield (time, MediaInfo.pretty_duration(time, show_millis=True)) | |
time += capture_interval | |
def select_sharpest_images( | |
media_info, | |
media_capture, | |
args, | |
num_groups=5): | |
"""Make `num_samples` captures and select `num_selected` captures out of these | |
based on blurriness and color variety. | |
""" | |
if num_groups is None: | |
num_groups = args.num_selected | |
# make sure num_selected is not too large | |
if args.num_selected > num_groups: | |
num_groups = args.num_selected | |
if args.num_selected > args.num_samples: | |
args.num_samples = args.num_selected | |
# make sure num_samples is large enough | |
if args.num_samples < args.num_selected or args.num_samples < num_groups: | |
args.num_samples = args.num_selected | |
num_groups = args.num_selected | |
if args.interval is not None: | |
total_delay = total_delay_seconds(media_info, args) | |
selected_duration = media_info.duration_seconds - total_delay | |
args.num_samples = math.floor(selected_duration / args.interval.total_seconds()) | |
args.num_selected = args.num_samples | |
num_groups = args.num_samples | |
square_side = math.ceil(math.sqrt(args.num_samples)) | |
if args.grid == DEFAULT_GRID_SIZE: | |
args.grid = Grid(square_side, square_side) | |
desired_size = grid_desired_size( | |
args.grid, | |
media_info, | |
width=args.vcs_width, | |
horizontal_margin=args.grid_horizontal_spacing) | |
blurs = [] | |
if args.manual_timestamps is None: | |
timestamps = timestamp_generator(media_info, args) | |
else: | |
timestamps = [(MediaInfo.pretty_to_seconds(x), x) for x in args.manual_timestamps] | |
for i, timestamp in enumerate(timestamps): | |
status = "Frames Processed: %s/%s" % ((i + 1), args.num_samples) | |
print(status, end="\r") | |
fd, filename = tempfile.mkstemp(suffix=".png") | |
media_capture.make_capture( | |
timestamp[1], | |
desired_size[0], | |
desired_size[1], | |
filename) | |
blurriness = media_capture.compute_blurriness(filename) | |
avg_color = media_capture.compute_avg_color(filename) | |
blurs += [ | |
Frame( | |
filename=filename, | |
blurriness=blurriness, | |
timestamp=timestamp[0], | |
avg_color=avg_color | |
) | |
] | |
os.close(fd) | |
time_sorted = sorted(blurs, key=lambda x: x.timestamp) | |
# group into num_selected groups | |
if num_groups > 1: | |
group_size = int(math.floor(len(time_sorted) / num_groups)) | |
groups = chunks(time_sorted, group_size) | |
# find top sharpest for each group | |
selected_items = [best(x) for x in groups] | |
else: | |
selected_items = time_sorted | |
selected_items = select_color_variety(selected_items, args.num_selected) | |
return selected_items, time_sorted | |
def select_color_variety(frames, num_selected): | |
"""Select captures so that they are not too similar to each other. | |
""" | |
avg_color_sorted = sorted(frames, key=lambda x: x.avg_color) | |
min_color = avg_color_sorted[0].avg_color | |
max_color = avg_color_sorted[-1].avg_color | |
color_span = max_color - min_color | |
min_color_distance = int(color_span * 0.05) | |
blurriness_sorted = sorted(frames, key=lambda x: x.blurriness, reverse=True) | |
selected_items = [] | |
unselected_items = [] | |
while blurriness_sorted: | |
frame = blurriness_sorted.pop() | |
if not selected_items: | |
selected_items += [frame] | |
else: | |
color_distance = min([abs(frame.avg_color - x.avg_color) for x in selected_items]) | |
if color_distance < min_color_distance: | |
# too close to existing selected frame | |
# don't select unless we run out of frames | |
unselected_items += [(frame, color_distance)] | |
else: | |
selected_items += [frame] | |
missing_items_count = num_selected - len(selected_items) | |
if missing_items_count > 0: | |
remaining_items = sorted(unselected_items, key=lambda x: x[0].blurriness) | |
selected_items += [x[0] for x in remaining_items[:missing_items_count]] | |
return selected_items | |
def best(captures): | |
"""Returns the least blurry capture | |
""" | |
return sorted(captures, key=lambda x: x.blurriness)[0] | |
def chunks(l, n): | |
""" Yield successive n-sized chunks from l. | |
""" | |
for i in range(0, len(l), n): | |
yield l[i:i + n] | |
def draw_metadata( | |
draw, | |
args, | |
header_line_height=None, | |
header_lines=None, | |
header_font=None, | |
header_font_color=None, | |
start_height=None): | |
"""Draw metadata header | |
""" | |
h = start_height | |
h += args.metadata_vertical_margin | |
for line in header_lines: | |
draw.text((args.metadata_horizontal_margin, h), line, font=header_font, fill=header_font_color) | |
h += header_line_height | |
h += args.metadata_vertical_margin | |
return h | |
def max_line_length( | |
media_info, | |
metadata_font, | |
header_margin, | |
width=DEFAULT_CONTACT_SHEET_WIDTH, | |
text=None): | |
"""Find the number of characters that fit in width with given font. | |
""" | |
if text is None: | |
text = media_info.filename | |
max_width = width - 2 * header_margin | |
max_length = 0 | |
for i in range(len(text) + 1): | |
text_chunk = text[:i] | |
text_width = 0 if len(text_chunk) == 0 else metadata_font.getsize(text_chunk)[0] | |
max_length = i | |
if text_width > max_width: | |
break | |
return max_length | |
def prepare_metadata_text_lines(media_info, header_font, header_margin, width, template_path=None): | |
"""Prepare the metadata header text and return a list containing each line. | |
""" | |
import string | |
import MediaInfoThumbs | |
Data = { 'VideoEncoder': '{}'.format(MediaInfoThumbs.VideoEncoder()), | |
'VideoCodec' : '{}'.format(MediaInfoThumbs.VideoFormatCodec()), | |
'VideoContainer' : '{}'.format(MediaInfoThumbs.VideoContainer()), | |
'VideoLength' : '{}'.format(MediaInfoThumbs.VideoLength()), | |
'VideoLength_Norm' : '{}'.format(MediaInfoThumbs.VideoLength_Norm()), | |
'VideoLengthMod' : '{}'.format(MediaInfoThumbs.VideoLengthMod()), | |
'VideoBitRate' : '{}'.format(MediaInfoThumbs.VideoBitRate()), | |
'VVideoBitRate' : '{}'.format(MediaInfoThumbs.VVideoBitRate()), | |
'VideoRange' : '{}'.format(MediaInfoThumbs.VideoRange()), | |
'Matrix' : '{}'.format(MediaInfoThumbs.Matrix()), | |
'VideoBitRateMod' : '{}'.format(MediaInfoThumbs.VideoBitRateMod()), | |
'Chroma' : '{}'.format(MediaInfoThumbs.ChromaSubsampling()), | |
'VideoHeight' : '{}'.format(MediaInfoThumbs.VideoHeight()), | |
'VideoWidth' : '{}'.format(MediaInfoThumbs.VideoWidth()), | |
'ColorSpace' : '{}'.format(MediaInfoThumbs.ColorSpace()), | |
'Format' : '{}'.format(MediaInfoThumbs.VideoFormat()), | |
'Scan' : '{}'.format(MediaInfoThumbs.ScanType()), | |
'FileSize' : '{}'.format(MediaInfoThumbs.FileSize()), | |
'FileName' : '{}'.format(MediaInfoThumbs.FileName()), | |
'FileSizeBytes' : '{}'.format(MediaInfoThumbs.FileSizeBytes()), | |
'FrameRate' : '{}'.format(MediaInfoThumbs.VideoFrameRate()), | |
'FrameRateFPS' : '{}'.format(MediaInfoThumbs.VideoFrameRateFPS()), | |
'BitDepth' : '{}'.format(MediaInfoThumbs.BitDepth()), | |
'AudioCodec' : '{}'.format(MediaInfoThumbs.AudioCodec()), | |
'Channels' : '{}'.format(MediaInfoThumbs.Channels()), | |
'Profile' : '{}'.format(MediaInfoThumbs.VideoProfile()), | |
'AudioBitRate' : '{}'.format(MediaInfoThumbs.AudioBitRate()), | |
'AudioSampleRate' : '{}'.format(MediaInfoThumbs.AudioSampleRate()), | |
'X' : 'x' | |
} | |
template = "" | |
if template_path is None: | |
template = """File: $FileName | |
Size: $FileSizeBytes bytes ($FileSize), Duration: $VideoLengthMod, avg.bitrate: $VideoBitRate | |
Audio: $AudioCodec, $AudioSampleRate, $Channels, $AudioBitRate | |
Video: $VideoCodec($Profile), $ColorSpace$Chroma($VideoRange, $Matrix), $VideoWidth$X$VideoHeight, $VVideoBitRate, $FrameRateFPS""" | |
else: | |
with open(template_path) as f: | |
template = f.read() | |
Merged = string.Template(template) | |
template=Merged.safe_substitute(Data) | |
#os.remove(Settings.ThumbPath) | |
params = media_info.template_attributes() | |
template = Template(template).render(params) | |
template_lines = template.split("\n") | |
template_lines = [x.strip() for x in template_lines if len(x) > 0] | |
header_lines = [] | |
for line in template_lines: | |
remaining_chars = line | |
while len(remaining_chars) > 0: | |
max_metadata_line_length = max_line_length( | |
media_info, | |
header_font, | |
header_margin, | |
width=width, | |
text=remaining_chars) | |
wraps = textwrap.wrap(remaining_chars, max_metadata_line_length) | |
header_lines.append(wraps[0]) | |
remaining_chars = remaining_chars[len(wraps[0]):].strip() | |
return header_lines | |
def compute_timestamp_position(args, w, h, text_size, desired_size, rectangle_hpadding, rectangle_vpadding): | |
"""Compute the (x,y) position of the upper left and bottom right points of the rectangle surrounding timestamp text. | |
""" | |
position = args.timestamp_position | |
x_offset = 0 | |
if position in [TimestampPosition.west, TimestampPosition.nw, TimestampPosition.sw]: | |
x_offset = args.timestamp_horizontal_margin | |
elif position in [TimestampPosition.north, TimestampPosition.center, TimestampPosition.south]: | |
x_offset = (desired_size[0] / 2) - (text_size[0] / 2) - rectangle_hpadding | |
else: | |
x_offset = desired_size[0] - text_size[0] - args.timestamp_horizontal_margin - 2 * rectangle_hpadding | |
y_offset = 0 | |
if position in [TimestampPosition.nw, TimestampPosition.north, TimestampPosition.ne]: | |
y_offset = args.timestamp_vertical_margin | |
elif position in [TimestampPosition.west, TimestampPosition.center, TimestampPosition.east]: | |
y_offset = (desired_size[1] / 2) - (text_size[1] / 2) - rectangle_vpadding | |
else: | |
y_offset = desired_size[1] - text_size[1] - args.timestamp_vertical_margin - 2 * rectangle_vpadding | |
upper_left = ( | |
w + x_offset, | |
h + y_offset | |
) | |
bottom_right = ( | |
upper_left[0] + text_size[0] + 2 * rectangle_hpadding, | |
upper_left[1] + text_size[1] + 2 * rectangle_vpadding | |
) | |
return upper_left, bottom_right | |
def compose_contact_sheet( | |
media_info, | |
frames, | |
args): | |
"""Creates a video contact sheet with the media information in a header | |
and the selected frames arranged on a mxn grid with optional timestamps | |
""" | |
desired_size = grid_desired_size( | |
args.grid, | |
media_info, | |
width=args.vcs_width, | |
horizontal_margin=args.grid_horizontal_spacing) | |
height = args.grid.y * (desired_size[1] + args.grid_vertical_spacing) - args.grid_vertical_spacing | |
try: | |
header_font = ImageFont.truetype(args.metadata_font, args.metadata_font_size) | |
except OSError: | |
if args.metadata_font == DEFAULT_METADATA_FONT: | |
header_font = ImageFont.load_default() | |
else: | |
raise | |
try: | |
timestamp_font = ImageFont.truetype(args.timestamp_font, args.timestamp_font_size) | |
except OSError: | |
if args.timestamp_font == DEFAULT_TIMESTAMP_FONT: | |
timestamp_font = ImageFont.load_default() | |
else: | |
raise | |
header_lines = prepare_metadata_text_lines( | |
media_info, | |
header_font, | |
args.metadata_horizontal_margin, | |
args.vcs_width, | |
template_path=args.metadata_template_path) | |
line_spacing_coefficient = 1.2 | |
header_line_height = int(args.metadata_font_size * line_spacing_coefficient) | |
header_height = 2 * args.metadata_margin + len(header_lines) * header_line_height | |
if args.metadata_position == "hidden": | |
header_height = 0 | |
final_image_width = args.vcs_width | |
final_image_height = height + header_height | |
transparent = (255, 255, 255, 0) | |
image = Image.new("RGBA", (final_image_width, final_image_height), args.background_color) | |
image_capture_layer = Image.new("RGBA", (final_image_width, final_image_height), transparent) | |
image_header_text_layer = Image.new("RGBA", (final_image_width, final_image_height), transparent) | |
image_timestamp_layer = Image.new("RGBA", (final_image_width, final_image_height), transparent) | |
image_timestamp_text_layer = Image.new("RGBA", (final_image_width, final_image_height), transparent) | |
draw_header_text_layer = ImageDraw.Draw(image_header_text_layer) | |
draw_timestamp_layer = ImageDraw.Draw(image_timestamp_layer) | |
draw_timestamp_text_layer = ImageDraw.Draw(image_timestamp_text_layer) | |
h = 0 | |
def draw_metadata_helper(): | |
"""Draw metadata with fixed arguments | |
""" | |
return draw_metadata( | |
draw_header_text_layer, | |
args, | |
header_line_height=header_line_height, | |
header_lines=header_lines, | |
header_font=header_font, | |
header_font_color=args.metadata_font_color, | |
start_height=h) | |
# draw metadata | |
if args.metadata_position == "top": | |
h = draw_metadata_helper() | |
# draw capture grid | |
w = 0 | |
frames = sorted(frames, key=lambda x: x.timestamp) | |
for i, frame in enumerate(frames): | |
f = Image.open(frame.filename) | |
f.putalpha(args.capture_alpha) | |
image_capture_layer.paste(f, (w, h)) | |
# show timestamp | |
if args.show_timestamp: | |
pretty_timestamp = MediaInfo.pretty_duration(frame.timestamp, show_centis=True) | |
text_size = timestamp_font.getsize(pretty_timestamp) | |
# draw rectangle | |
rectangle_hpadding = args.timestamp_horizontal_padding | |
rectangle_vpadding = args.timestamp_vertical_padding | |
upper_left, bottom_right = compute_timestamp_position(args, w, h, text_size, desired_size, | |
rectangle_hpadding, rectangle_vpadding) | |
if not args.timestamp_border_mode: | |
draw_timestamp_layer.rectangle( | |
[upper_left, bottom_right], | |
fill=args.timestamp_background_color | |
) | |
else: | |
offset_factor = args.timestamp_border_size | |
offsets = [ | |
(1, 0), | |
(-1, 0), | |
(0, 1), | |
(0, -1), | |
(1, 1), | |
(1, -1), | |
(-1, 1), | |
(-1, -1) | |
] | |
final_offsets = [] | |
for offset_counter in range(1, offset_factor + 1): | |
final_offsets += [(x[0] * offset_counter, x[1] * offset_counter) for x in offsets] | |
for offset in final_offsets: | |
# draw border first | |
draw_timestamp_text_layer.text( | |
( | |
upper_left[0] + rectangle_hpadding + offset[0], | |
upper_left[1] + rectangle_vpadding + offset[1] | |
), | |
pretty_timestamp, | |
font=timestamp_font, | |
fill=args.timestamp_border_color | |
) | |
# draw timestamp | |
draw_timestamp_text_layer.text( | |
( | |
upper_left[0] + rectangle_hpadding, | |
upper_left[1] + rectangle_vpadding | |
), | |
pretty_timestamp, | |
font=timestamp_font, | |
fill=args.timestamp_font_color | |
) | |
# update x position for next frame | |
w += desired_size[0] + args.grid_horizontal_spacing | |
# update y position | |
if (i + 1) % args.grid.x == 0: | |
h += desired_size[1] + args.grid_vertical_spacing | |
# update x position | |
if (i + 1) % args.grid.x == 0: | |
w = 0 | |
# draw metadata | |
if args.metadata_position == "bottom": | |
h -= args.grid_vertical_spacing | |
h = draw_metadata_helper() | |
# alpha blend | |
out_image = Image.alpha_composite(image, image_capture_layer) | |
out_image = Image.alpha_composite(out_image, image_header_text_layer) | |
out_image = Image.alpha_composite(out_image, image_timestamp_layer) | |
out_image = Image.alpha_composite(out_image, image_timestamp_text_layer) | |
return out_image | |
def save_image(args, image, media_info, output_path): | |
"""Save the image to `output_path` | |
""" | |
image = image.convert("RGB") | |
try: | |
image.save(output_path, optimize=True, quality=args.image_quality) | |
return True | |
except KeyError: | |
return False | |
def cleanup(frames): | |
"""Delete temporary captures | |
""" | |
for frame in frames: | |
try: | |
os.unlink(frame.filename) | |
except: | |
pass | |
def print_template_attributes(): | |
"""Display all the available template attributes in a tabular format | |
""" | |
table = MediaInfo.list_template_attributes() | |
tab = texttable.Texttable() | |
tab.set_cols_dtype(["t", "t", "t"]) | |
rows = [[x["name"], x["description"], x["example"]] for x in table] | |
tab.add_rows(rows, header=False) | |
tab.header(["Attribute name", "Description", "Example"]) | |
print(tab.draw()) | |
def mxn_type(string): | |
"""Type parser for argparse. Argument of type "mxn" will be converted to Grid(m, n). | |
An exception will be thrown if the argument is not of the required form | |
""" | |
try: | |
split = string.split("x") | |
assert (len(split) == 2) | |
m = int(split[0]) | |
assert (m > 0) | |
n = int(split[1]) | |
assert (n > 0) | |
return Grid(m, n) | |
except (IndexError, ValueError, AssertionError): | |
error = "Grid must be of the form mxn, where m is the number of columns and n is the number of rows." | |
raise argparse.ArgumentTypeError(error) | |
def metadata_position_type(string): | |
"""Type parser for argparse. Argument of type string must be one of ["top", "bottom", "hidden"]. | |
An exception will be thrown if the argument is not one of these. | |
""" | |
valid_metadata_positions = ["top", "bottom", "hidden"] | |
lowercase_position = string.lower() | |
if lowercase_position in valid_metadata_positions: | |
return lowercase_position | |
else: | |
error = 'Metadata header position must be one of %s' % (str(valid_metadata_positions, )) | |
raise argparse.ArgumentTypeError(error) | |
def hex_color_type(string): | |
"""Type parser for argparse. Argument must be an hexadecimal number representing a color. | |
For example 'AABBCC' (RGB) or 'AABBCCFF' (RGBA). An exception will be raised if the argument | |
is not of that form. | |
""" | |
try: | |
components = tuple(bytearray.fromhex(string)) | |
if len(components) == 3: | |
components += (255,) | |
c = Color(*components) | |
return c | |
except: | |
error = "Color must be an hexadecimal number, for example 'AABBCC'" | |
raise argparse.ArgumentTypeError(error) | |
def manual_timestamps(string): | |
"""Type parser for argparse. Argument must be a comma-separated list of frame timestamps. | |
For example 1:11:11.111,2:22:22.222 | |
""" | |
try: | |
timestamps = string.split(",") | |
timestamps = [x.strip() for x in timestamps if x] | |
# check whether timestamps are valid | |
for t in timestamps: | |
MediaInfo.pretty_to_seconds(t) | |
return timestamps | |
except Exception as e: | |
print(e) | |
error = "Manual frame timestamps must be comma-separated and of the form h:mm:ss.mmmm" | |
raise argparse.ArgumentTypeError(error) | |
def timestamp_position_type(string): | |
"""Type parser for argparse. Argument must be a valid timestamp position""" | |
try: | |
return getattr(TimestampPosition, string) | |
except AttributeError: | |
error = "Invalid timestamp position: %s. Valid positions are: %s" % (string, VALID_TIMESTAMP_POSITIONS) | |
raise argparse.ArgumentTypeError(error) | |
def interval_type(string): | |
"""Type parser for argparse. Argument must be a valid interval format. | |
Supports any format supported by `parsedatetime`, including: | |
* "30sec" (every 30 seconds) | |
* "5 minutes" (every 5 minutes) | |
* "1h" (every hour) | |
* "2 hours 1 min and 30 seconds" | |
""" | |
m = datetime.datetime.min | |
cal = parsedatetime.Calendar() | |
interval = cal.parseDT(string, sourceTime=m)[0] - m | |
if interval == m: | |
error = "Invalid interval format: {}".format(string) | |
raise argparse.ArgumentTypeError(error) | |
return interval | |
def error(message): | |
"""Print an error message.""" | |
print("[ERROR] %s" % (message,)) | |
def error_exit(message): | |
"""Print an error message and exit""" | |
error(message) | |
sys.exit(-1) | |
def main(): | |
"""Program entry point | |
""" | |
parser = argparse.ArgumentParser(description="Create a video contact sheet", | |
formatter_class=argparse.ArgumentDefaultsHelpFormatter) | |
parser.add_argument("filenames", nargs="+") | |
parser.add_argument( | |
"-o", "--output", | |
help="save to output file", | |
dest="output_path") | |
parser.add_argument( | |
"--start-delay-percent", | |
help="do not capture frames in the first n percent of total time", | |
dest="start_delay_percent", | |
type=int, | |
default=DEFAULT_START_DELAY_PERCENT) | |
parser.add_argument( | |
"--end-delay-percent", | |
help="do not capture frames in the last n percent of total time", | |
dest="end_delay_percent", | |
type=int, | |
default=DEFAULT_END_DELAY_PERCENT) | |
parser.add_argument( | |
"--delay-percent", | |
help="do not capture frames in the first and last n percent of total time", | |
dest="delay_percent", | |
type=int, | |
default=DEFAULT_DELAY_PERCENT) | |
parser.add_argument( | |
"--grid-spacing", | |
help="number of pixels spacing captures both vertically and horizontally", | |
dest="grid_spacing", | |
type=int, | |
default=DEFAULT_GRID_SPACING) | |
parser.add_argument( | |
"--grid-horizontal-spacing", | |
help="number of pixels spacing captures horizontally", | |
dest="grid_horizontal_spacing", | |
type=int, | |
default=DEFAULT_GRID_HORIZONTAL_SPACING) | |
parser.add_argument( | |
"--grid-vertical-spacing", | |
help="number of pixels spacing captures vertically", | |
dest="grid_vertical_spacing", | |
type=int, | |
default=DEFAULT_GRID_VERTICAL_SPACING) | |
parser.add_argument( | |
"-w", "--width", | |
help="width of the generated contact sheet", | |
dest="vcs_width", | |
type=int, | |
default=DEFAULT_CONTACT_SHEET_WIDTH) | |
parser.add_argument( | |
"-g", "--grid", | |
help="display frames on a mxn grid (for example 4x5)", | |
dest="grid", | |
type=mxn_type, | |
default=DEFAULT_GRID_SIZE) | |
parser.add_argument( | |
"-s", "--num-samples", | |
help="number of samples", | |
dest="num_samples", | |
type=int, | |
default=None) | |
parser.add_argument( | |
"-t", "--show-timestamp", | |
action="store_true", | |
help="display timestamp for each frame", | |
dest="show_timestamp", | |
default=True) | |
parser.add_argument( | |
"--metadata-font-size", | |
help="size of the font used for metadata", | |
dest="metadata_font_size", | |
type=int, | |
default=DEFAULT_METADATA_FONT_SIZE) | |
parser.add_argument( | |
"--metadata-font", | |
help="TTF font used for metadata", | |
dest="metadata_font", | |
default=DEFAULT_METADATA_FONT) | |
parser.add_argument( | |
"--timestamp-font-size", | |
help="size of the font used for timestamps", | |
dest="timestamp_font_size", | |
type=int, | |
default=DEFAULT_TIMESTAMP_FONT_SIZE) | |
parser.add_argument( | |
"--timestamp-font", | |
help="TTF font used for timestamps", | |
dest="timestamp_font", | |
default=DEFAULT_TIMESTAMP_FONT) | |
parser.add_argument( | |
"--metadata-position", | |
help="Position of the metadata header. Must be one of ['top', 'bottom', 'hidden']", | |
dest="metadata_position", | |
type=metadata_position_type, | |
default=DEFAULT_METADATA_POSITION) | |
parser.add_argument( | |
"--background-color", | |
help="Color of the background in hexadecimal, for example AABBCC", | |
dest="background_color", | |
type=hex_color_type, | |
default=hex_color_type(DEFAULT_BACKGROUND_COLOR)) | |
parser.add_argument( | |
"--metadata-font-color", | |
help="Color of the metadata font in hexadecimal, for example AABBCC", | |
dest="metadata_font_color", | |
type=hex_color_type, | |
default=hex_color_type(DEFAULT_METADATA_FONT_COLOR)) | |
parser.add_argument( | |
"--timestamp-font-color", | |
help="Color of the timestamp font in hexadecimal, for example AABBCC", | |
dest="timestamp_font_color", | |
type=hex_color_type, | |
default=hex_color_type(DEFAULT_TIMESTAMP_FONT_COLOR)) | |
parser.add_argument( | |
"--timestamp-background-color", | |
help="Color of the timestamp background rectangle in hexadecimal, for example AABBCC", | |
dest="timestamp_background_color", | |
type=hex_color_type, | |
default=hex_color_type(DEFAULT_TIMESTAMP_BACKGROUND_COLOR)) | |
parser.add_argument( | |
"--timestamp-border-color", | |
help="Color of the timestamp border in hexadecimal, for example AABBCC", | |
dest="timestamp_border_color", | |
type=hex_color_type, | |
default=hex_color_type(DEFAULT_TIMESTAMP_BORDER_COLOR)) | |
parser.add_argument( | |
"--template", | |
help="Path to metadata template file", | |
dest="metadata_template_path", | |
default=None) | |
parser.add_argument( | |
"-m", "--manual", | |
help="Comma-separated list of frame timestamps to use, for example 1:11:11.111,2:22:22.222", | |
dest="manual_timestamps", | |
type=manual_timestamps, | |
default=None) | |
parser.add_argument( | |
"-v", "--verbose", | |
action="store_true", | |
help="display verbose messages", | |
dest="is_verbose") | |
parser.add_argument( | |
"-a", "--accurate", | |
action="store_true", | |
help="""Make accurate captures. This capture mode is way slower than the default one | |
but it helps when capturing frames from HEVC videos.""", | |
dest="is_accurate") | |
parser.add_argument( | |
"-A", "--accurate-delay-seconds", | |
type=int, | |
default=DEFAULT_ACCURATE_DELAY_SECONDS, | |
help="""Fast skip to N seconds before capture time, then do accurate capture | |
(decodes N seconds of video before each capture). This is used with accurate capture mode only.""", | |
dest="accurate_delay_seconds") | |
parser.add_argument( | |
"--metadata-margin", | |
type=int, | |
default=DEFAULT_METADATA_MARGIN, | |
help="Margin (in pixels) in the metadata header.", | |
dest="metadata_margin") | |
parser.add_argument( | |
"--metadata-horizontal-margin", | |
type=int, | |
default=DEFAULT_METADATA_HORIZONTAL_MARGIN, | |
help="Horizontal margin (in pixels) in the metadata header.", | |
dest="metadata_horizontal_margin") | |
parser.add_argument( | |
"--metadata-vertical-margin", | |
type=int, | |
default=DEFAULT_METADATA_VERTICAL_MARGIN, | |
help="Vertical margin (in pixels) in the metadata header.", | |
dest="metadata_vertical_margin") | |
parser.add_argument( | |
"--timestamp-horizontal-padding", | |
type=int, | |
default=DEFAULT_TIMESTAMP_HORIZONTAL_PADDING, | |
help="Horizontal padding (in pixels) for timestamps.", | |
dest="timestamp_horizontal_padding") | |
parser.add_argument( | |
"--timestamp-vertical-padding", | |
type=int, | |
default=DEFAULT_TIMESTAMP_VERTICAL_PADDING, | |
help="Vertical padding (in pixels) for timestamps.", | |
dest="timestamp_vertical_padding") | |
parser.add_argument( | |
"--timestamp-horizontal-margin", | |
type=int, | |
default=DEFAULT_TIMESTAMP_HORIZONTAL_MARGIN, | |
help="Horizontal margin (in pixels) for timestamps.", | |
dest="timestamp_horizontal_margin") | |
parser.add_argument( | |
"--timestamp-vertical-margin", | |
type=int, | |
default=DEFAULT_TIMESTAMP_VERTICAL_MARGIN, | |
help="Vertical margin (in pixels) for timestamps.", | |
dest="timestamp_vertical_margin") | |
parser.add_argument( | |
"--quality", | |
type=int, | |
default=DEFAULT_IMAGE_QUALITY, | |
help="Output image quality. Must be an integer in the range 0-100. 100 = best quality.", | |
dest="image_quality") | |
parser.add_argument( | |
"-f", "--format", | |
type=str, | |
default=DEFAULT_IMAGE_FORMAT, | |
help="Output image format. Can be any format supported by pillow. For example 'png' or 'jpg'.", | |
dest="image_format") | |
parser.add_argument( | |
"-T", "--timestamp-position", | |
type=timestamp_position_type, | |
default=DEFAULT_TIMESTAMP_POSITION, | |
help="Timestamp position. Must be one of %s." % (VALID_TIMESTAMP_POSITIONS,), | |
dest="timestamp_position") | |
parser.add_argument( | |
"-r", "--recursive", | |
action="store_true", | |
help="Process every file in the specified directory recursively.", | |
dest="recursive") | |
parser.add_argument( | |
"--timestamp-border-mode", | |
action="store_true", | |
help="Draw timestamp text with a border instead of the default rectangle.", | |
dest="timestamp_border_mode", | |
default=True) | |
parser.add_argument( | |
"--timestamp-border-size", | |
type=int, | |
default=DEFAULT_TIMESTAMP_BORDER_SIZE, | |
help="Size of the timestamp border in pixels (used only with --timestamp-border-mode).", | |
dest="timestamp_border_size") | |
parser.add_argument( | |
"--capture-alpha", | |
type=int, | |
default=DEFAULT_CAPTURE_ALPHA, | |
help="Alpha channel value for the captures (transparency in range [0, 255]). Defaults to 255 (opaque)", | |
dest="capture_alpha") | |
parser.add_argument( | |
"--version", | |
action="version", | |
version="%(prog)s version {version}".format(version=__version__)) | |
parser.add_argument( | |
"--list-template-attributes", | |
action="store_true", | |
dest="list_template_attributes") | |
parser.add_argument( | |
"--frame-type", | |
type=str, | |
default=DEFAULT_FRAME_TYPE, | |
help="Frame type passed to ffmpeg 'select=eq(pict_type,FRAME_TYPE)' filter. Should be one of ('I', 'B', 'P') or the special type 'key' which will use the 'select=key' filter instead.", | |
dest="frame_type") | |
parser.add_argument( | |
"--interval", | |
type=interval_type, | |
default=DEFAULT_INTERVAL, | |
help="Capture frames at specified interval. Interval format is any string supported by `parsedatetime`. For example '5m', '3 minutes 5 seconds', '1 hour 15 min and 20 sec' etc.", | |
dest="interval") | |
parser.add_argument( | |
"--ignore-errors", | |
action="store_true", | |
help="Ignore any error encountered while processing files recursively and continue to the next file.", | |
dest="ignore_errors") | |
parser.add_argument( | |
"--no-overwrite", | |
action="store_true", | |
help="Do not overwrite output file if it already exists, simply ignore this file and continue processing other unprocessed files.", | |
dest="no_overwrite" | |
) | |
args = parser.parse_args() | |
if args.list_template_attributes: | |
print_template_attributes() | |
sys.exit(0) | |
if args.recursive: | |
for path in args.filenames: | |
for root, subdirs, files in os.walk(path): | |
for f in files: | |
filepath = os.path.join(root, f) | |
try: | |
process_file(filepath, args) | |
except Exception: | |
if not args.ignore_errors: | |
raise | |
else: | |
print("Failed to Process: {}".format(filepath), file=sys.stderr) | |
else: | |
for path in args.filenames: | |
if os.path.isdir(path): | |
for filepath in os.listdir(path): | |
abs_filepath = os.path.join(path, filepath) | |
if not os.path.isdir(abs_filepath): | |
process_file(abs_filepath, args) | |
else: | |
files_to_process = glob(path) | |
if len(files_to_process) == 0: | |
files_to_process = [path] | |
for filename in files_to_process: | |
process_file(filename, args) | |
def process_file(path, args): | |
"""Generate a video contact sheet for the file at given path | |
""" | |
#with open(Settings.ThumbPath, 'w') as Thumbs: | |
# Thumbs.write(path) | |
if not os.path.exists(path): | |
if args.ignore_errors: | |
print("File Does Not Exist, Skipping: {}".format(path)) | |
return | |
else: | |
error_message = "File does not exist: {}".format(path) | |
error_exit(error_message) | |
media_info = MediaInfo( | |
path, | |
verbose=args.is_verbose) | |
media_capture = MediaCapture( | |
path, | |
accurate=args.is_accurate, | |
skip_delay_seconds=args.accurate_delay_seconds, | |
frame_type=args.frame_type | |
) | |
output_path = args.output_path | |
if not output_path: | |
output_path = media_info.filename + "." + args.image_format | |
if args.no_overwrite: | |
if os.path.exists(output_path): | |
print("Output File Already Exists, Skipping: {}".format(output_path)) | |
return | |
# from MediaInfoThumbs import Path | |
#Path() | |
# metadata margins | |
if not args.metadata_margin == DEFAULT_METADATA_MARGIN: | |
args.metadata_horizontal_margin = args.metadata_margin | |
args.metadata_vertical_margin = args.metadata_margin | |
args.num_selected = args.grid.x * args.grid.y | |
# manual frame selection | |
if args.manual_timestamps is not None: | |
mframes_size = len(args.manual_timestamps) | |
grid_size = args.grid.x * args.grid.y | |
args.num_selected = mframes_size | |
args.num_samples = mframes_size | |
if not mframes_size == grid_size: | |
# specified number of columns | |
y = math.ceil(mframes_size / args.grid.x) | |
args.grid = Grid(args.grid.x, y) | |
if args.num_selected < 1: | |
error = "One of --grid, --manual must be specified" | |
raise argparse.ArgumentTypeError(error) | |
if args.num_samples is None: | |
args.num_samples = args.num_selected | |
if args.delay_percent is not None: | |
args.start_delay_percent = args.delay_percent | |
args.end_delay_percent = args.delay_percent | |
if args.grid_spacing is not None: | |
args.grid_horizontal_spacing = args.grid_spacing | |
args.grid_vertical_spacing = args.grid_spacing | |
selected_frames, temp_frames = select_sharpest_images(media_info, media_capture, args) | |
print("Frames Processed: ") | |
image = compose_contact_sheet(media_info, selected_frames, args) | |
is_save_successful = save_image(args, image, media_info, output_path) | |
#print("Cleaning up temporary files...") | |
cleanup(temp_frames) | |
#os.remove(Settings.ThumbPath) | |
if not is_save_successful: | |
error_exit("Unsupported image format: %s." % (args.image_format,)) | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment