diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..4c5b0006 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,70 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.0.1 + hooks: + # - id: check-added-large-files + - id: check-case-conflict + - id: check-json + - id: check-merge-conflict + - id: check-yaml + - id: end-of-file-fixer + exclude: "__snapshots__/" + - id: fix-encoding-pragma + args: [--remove] + - id: mixed-line-ending + - id: trailing-whitespace + args: [--markdown-linebreak-ext=md] + exclude: "__snapshots__/" + - id: pretty-format-json + args: [--autofix] + exclude: "__snapshots__/" + # this is not technically always safe but usually is + # use comments `# isort: off` and `# isort: on` to disable/re-enable isort + - repo: https://github.com/pycqa/isort + rev: 5.12.0 + hooks: + - id: isort + args: [--line-length=240, --profile=black] + + # this is slightly dangerous because python imports have side effects + # and this tool removes unused imports, which may be providing + # necessary side effects for the code to run + - repo: https://github.com/PyCQA/autoflake + rev: v1.6.1 + hooks: + - id: autoflake + args: + - "--in-place" + - "--expand-star-imports" + - "--remove-duplicate-keys" + - "--remove-unused-variables" + - "--remove-all-unused-imports" + + - repo: https://github.com/psf/black + rev: 22.8.0 + hooks: + - id: black + args: [--line-length=120, --exclude=""] + + - repo: local + hooks: + - id: mypy + name: mypy + entry: mypy + language: system + types: [python] + exclude: migrations/|commands/|scripts/|sandbox/|samples|sdk|examples + args: + [ + --pretty, + --show-error-codes, + --implicit-optional, + --follow-imports=silent, + --warn-redundant-casts, + --warn-unused-ignores, + --disallow-any-generics, + --check-untyped-defs, + --no-implicit-reexport, + --disallow-untyped-defs, + --install-types, + ] diff --git a/doc/html/_static/basic.css b/doc/html/_static/basic.css index c41d718e..af807072 100644 --- a/doc/html/_static/basic.css +++ b/doc/html/_static/basic.css @@ -760,4 +760,4 @@ div.math:hover a.headerlink { #top-link { display: none; } -} \ No newline at end of file +} diff --git a/doc/html/_static/documentation_options.js b/doc/html/_static/documentation_options.js index 6d865102..58357acc 100644 --- a/doc/html/_static/documentation_options.js +++ b/doc/html/_static/documentation_options.js @@ -7,4 +7,4 @@ var DOCUMENTATION_OPTIONS = { HAS_SOURCE: true, SOURCELINK_SUFFIX: '.txt', NAVIGATION_WITH_KEYS: false -}; \ No newline at end of file +}; diff --git a/doc/html/_static/language_data.js b/doc/html/_static/language_data.js index 5266fb19..f891ac19 100644 --- a/doc/html/_static/language_data.js +++ b/doc/html/_static/language_data.js @@ -13,7 +13,7 @@ var stopwords = ["a","and","are","as","at","be","but","by","for","if","in","into","is","it","near","no","not","of","on","or","such","that","the","their","then","there","these","they","this","to","was","will","with"]; -/* Non-minified version JS is _stemmer.js if file is provided */ +/* Non-minified version JS is _stemmer.js if file is provided */ /** * Porter Stemmer */ @@ -293,5 +293,3 @@ function splitQuery(query) { } return result; } - - diff --git a/doc/html/_static/nature.css b/doc/html/_static/nature.css index 5fb55b12..43a6b92b 100644 --- a/doc/html/_static/nature.css +++ b/doc/html/_static/nature.css @@ -8,11 +8,11 @@ * :license: BSD, see LICENSE for details. * */ - + @import url("basic.css"); - + /* -- page layout ----------------------------------------------------------- */ - + body { font-family: Arial, sans-serif; font-size: 100%; @@ -34,18 +34,18 @@ div.bodywrapper { hr { border: 1px solid #B1B4B6; } - + div.document { background-color: #eee; } - + div.body { background-color: #ffffff; color: #3E4349; padding: 0 30px 30px 30px; font-size: 0.9em; } - + div.footer { color: #555; width: 100%; @@ -53,12 +53,12 @@ div.footer { text-align: center; font-size: 75%; } - + div.footer a { color: #444; text-decoration: underline; } - + div.related { background-color: #6BA81E; line-height: 32px; @@ -66,11 +66,11 @@ div.related { text-shadow: 0px 1px 0 #444; font-size: 0.9em; } - + div.related a { color: #E2F3CC; } - + div.sphinxsidebar { font-size: 0.75em; line-height: 1.5em; @@ -79,7 +79,7 @@ div.sphinxsidebar { div.sphinxsidebarwrapper{ padding: 20px 0; } - + div.sphinxsidebar h3, div.sphinxsidebar h4 { font-family: Arial, sans-serif; @@ -95,30 +95,30 @@ div.sphinxsidebar h4 { div.sphinxsidebar h4{ font-size: 1.1em; } - + div.sphinxsidebar h3 a { color: #444; } - - + + div.sphinxsidebar p { color: #888; padding: 5px 20px; } - + div.sphinxsidebar p.topless { } - + div.sphinxsidebar ul { margin: 10px 20px; padding: 0; color: #000; } - + div.sphinxsidebar a { color: #444; } - + div.sphinxsidebar input { border: 1px solid #ccc; font-family: sans-serif; @@ -131,17 +131,17 @@ div.sphinxsidebar .searchformwrapper { } /* -- body styles ----------------------------------------------------------- */ - + a { color: #005B81; text-decoration: none; } - + a:hover { color: #E32E00; text-decoration: underline; } - + div.body h1, div.body h2, div.body h3, @@ -156,30 +156,30 @@ div.body h6 { padding: 5px 0 5px 10px; text-shadow: 0px 1px 0 white } - + div.body h1 { border-top: 20px solid white; margin-top: 0; font-size: 200%; } div.body h2 { font-size: 150%; background-color: #C8D5E3; } div.body h3 { font-size: 120%; background-color: #D8DEE3; } div.body h4 { font-size: 110%; background-color: #D8DEE3; } div.body h5 { font-size: 100%; background-color: #D8DEE3; } div.body h6 { font-size: 100%; background-color: #D8DEE3; } - + a.headerlink { color: #c60f0f; font-size: 0.8em; padding: 0 4px 0 4px; text-decoration: none; } - + a.headerlink:hover { background-color: #c60f0f; color: white; } - + div.body p, div.body dd, div.body li { line-height: 1.5em; } - + div.admonition p.admonition-title + p { display: inline; } @@ -192,29 +192,29 @@ div.note { background-color: #eee; border: 1px solid #ccc; } - + div.seealso { background-color: #ffc; border: 1px solid #ff6; } - + div.topic { background-color: #eee; } - + div.warning { background-color: #ffe4e4; border: 1px solid #f66; } - + p.admonition-title { display: inline; } - + p.admonition-title:after { content: ":"; } - + pre { padding: 10px; background-color: White; @@ -226,7 +226,7 @@ pre { -webkit-box-shadow: 1px 1px 1px #d8d8d8; -moz-box-shadow: 1px 1px 1px #d8d8d8; } - + code { background-color: #ecf0f3; color: #222; @@ -249,4 +249,4 @@ div.code-block-caption { background-color: #ddd; color: #222; border: 1px solid #C6C9CB; -} \ No newline at end of file +} diff --git a/doc/html/_static/pygments.css b/doc/html/_static/pygments.css index 20c4814d..9b7363c7 100644 --- a/doc/html/_static/pygments.css +++ b/doc/html/_static/pygments.css @@ -66,4 +66,4 @@ .highlight .vg { color: #bb60d5 } /* Name.Variable.Global */ .highlight .vi { color: #bb60d5 } /* Name.Variable.Instance */ .highlight .vm { color: #bb60d5 } /* Name.Variable.Magic */ -.highlight .il { color: #208050 } /* Literal.Number.Integer.Long */ \ No newline at end of file +.highlight .il { color: #208050 } /* Literal.Number.Integer.Long */ diff --git a/doc/html/genindex.html b/doc/html/genindex.html index b24ee611..82d844aa 100644 --- a/doc/html/genindex.html +++ b/doc/html/genindex.html @@ -14,7 +14,7 @@ - + +
- +

Index

@@ -54,7 +54,7 @@

Index

| T | V | Z - +

A

@@ -262,7 +262,7 @@

Navigation

  • modules |
  • - + - \ No newline at end of file + diff --git a/doc/html/index.html b/doc/html/index.html index 55d31a94..acc39697 100644 --- a/doc/html/index.html +++ b/doc/html/index.html @@ -13,7 +13,7 @@ - + +
    - +

    ffmpeg-python: Python bindings for FFmpeg

    @@ -751,7 +751,7 @@

    Navigation

  • modules |
  • - +
    - \ No newline at end of file + diff --git a/doc/html/py-modindex.html b/doc/html/py-modindex.html index 00af8197..b2c67fec 100644 --- a/doc/html/py-modindex.html +++ b/doc/html/py-modindex.html @@ -14,7 +14,7 @@ - + @@ -17,7 +17,7 @@ - + +
    - +

    Search

    @@ -57,9 +57,9 @@

    Search

    - +
    - +
    @@ -80,7 +80,7 @@

    Navigation

  • modules |
  • - +
    - \ No newline at end of file + diff --git a/doc/html/searchindex.js b/doc/html/searchindex.js index a8825d29..9dff8dc5 100644 --- a/doc/html/searchindex.js +++ b/doc/html/searchindex.js @@ -1 +1 @@ -Search.setIndex({docnames:["index"],envversion:{"sphinx.domains.c":1,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":1,"sphinx.domains.javascript":1,"sphinx.domains.math":2,"sphinx.domains.python":1,"sphinx.domains.rst":1,"sphinx.domains.std":1,sphinx:56},filenames:["index.rst"],objects:{"":{ffmpeg:[0,0,0,"-"]},"ffmpeg.Stream":{audio:[0,3,1,""],video:[0,3,1,""],view:[0,3,1,""]},ffmpeg:{Error:[0,1,1,""],Stream:[0,2,1,""],colorchannelmixer:[0,4,1,""],compile:[0,4,1,""],concat:[0,4,1,""],crop:[0,4,1,""],drawbox:[0,4,1,""],drawtext:[0,4,1,""],filter:[0,4,1,""],filter_:[0,4,1,""],filter_multi_output:[0,4,1,""],get_args:[0,4,1,""],hflip:[0,4,1,""],hue:[0,4,1,""],input:[0,4,1,""],merge_outputs:[0,4,1,""],output:[0,4,1,""],overlay:[0,4,1,""],overwrite_output:[0,4,1,""],probe:[0,4,1,""],run:[0,4,1,""],run_async:[0,4,1,""],setpts:[0,4,1,""],trim:[0,4,1,""],vflip:[0,4,1,""],view:[0,4,1,""],zoompan:[0,4,1,""]}},objnames:{"0":["py","module","Python module"],"1":["py","exception","Python exception"],"2":["py","class","Python class"],"3":["py","method","Python method"],"4":["py","function","Python function"]},objtypes:{"0":"py:module","1":"py:exception","2":"py:class","3":"py:method","4":"py:function"},terms:{"break":0,"case":0,"class":0,"default":0,"final":0,"function":0,"return":0,"true":0,"while":0,For:0,PTS:0,The:0,These:0,Used:0,accept:0,access:0,acodec:0,action:0,activ:0,adjust:0,aecho:0,after:0,alia:0,all:0,allow:0,alpha:0,also:0,altern:0,alwai:0,angl:0,ani:0,anoth:0,appli:0,arab:0,area:0,arg:0,argument:0,around:0,ascent:0,asetpt:0,ask:0,aspect:0,astyp:0,asynchron:0,atom:0,attempt:0,audio:0,audio_bitr:0,author:0,automat:0,avoid:0,axi:0,background:0,base:0,baselin:0,basetim:0,befor:0,behavior:0,behind:0,below:0,between:0,black:0,blend:0,border:0,bordercolor:0,borderw:0,both:0,box:0,boxborderw:0,boxcolor:0,bracket:0,bright:0,build:0,built:0,call:0,can:0,captur:0,capture_stderr:0,capture_stdout:0,care:0,certain:0,chang:0,channel:0,charact:0,check:0,child:0,chroma:0,clip:0,close:0,cmd:0,code:0,codec:0,collid:0,color:0,colorchannelmix:0,com:0,combin:0,command:0,commnad:0,common:0,commun:0,compil:0,concat:0,concaten:0,configur:0,confus:0,connect:0,constant:0,construct:0,consult:0,contain:0,continu:0,convert:0,coord:0,coordin:0,corner:0,correctli:0,correspond:0,count:0,creat:0,crop:0,crop_bitmap:0,custom:0,dar:0,data:0,debug:0,degre:0,deprec:0,descent:0,detail:0,dictionari:0,differ:0,dilemma:0,directli:0,disabl:0,displai:0,distanc:0,document:0,downstream:0,draw:0,drawbox:0,drawn:0,drawtext:0,drop:0,due:0,durat:0,dure:0,dynam:0,each:0,edg:0,effect:0,either:0,empti:0,emul:0,enabl:0,encod:0,encount:0,end:0,end_fram:0,end_pt:0,endal:0,eof:0,eof_act:0,equival:0,err:0,error:0,escape_text:0,etc:0,eval:0,evalu:0,even:0,exactli:0,exampl:0,except:0,exit:0,expand:0,expans:0,explicitli:0,expr:0,express:0,fail:0,fallback:0,fals:0,famili:0,ffmpeg_arg:0,ffprobe:0,file:0,filenam:0,filter:0,filter_:0,filter_multi_output:0,filter_nam:0,first:0,fix:0,fix_bound:0,flag:0,flip:0,follow:0,font:0,fontcolor:0,fontcolor_expr:0,fontconfig:0,fontfil:0,fontsiz:0,forc:0,force_autohint:0,format:0,fps:0,frame:0,frame_num:0,from:0,frombuff:0,ft_load_:0,ft_load_flag:0,gbrp:0,gener:0,get_arg:0,github:0,given:0,glyph:0,graph:0,greater:0,grid:0,handl:0,has:0,have:0,hd720:0,height:0,heigth:0,hflip:0,higher:0,highest:0,horizont:0,hour:0,how:0,hsub:0,http:0,hue:0,huge:0,ignore_global_advance_width:0,ignore_transform:0,imag:0,immedi:0,implement:0,in_byt:0,in_filenam:0,in_fram:0,includ:0,incom:0,independ:0,index:0,inform:0,init:0,initi:0,input:0,input_data:0,instead:0,interpret:0,intrins:0,invalid:0,invert:0,invok:0,its:0,join:0,json:0,just:0,kept:0,keyword:0,kkroen:0,kwarg:0,label:0,last:0,later:0,layout:0,left:0,level:0,libfontconfig:0,libfreetyp:0,libfribidi:0,librari:0,line:0,line_h:0,line_spac:0,linear_design:0,list:0,load:0,longest:0,lowest:0,luma:0,mai:0,main:0,main_h:0,main_parent_nod:0,main_w:0,mandatori:0,mani:0,manual:0,map:0,max:0,max_glyph_a:0,max_glyph_d:0,max_glyph_h:0,max_glyph_w:0,maximum:0,mean:0,merge_output:0,messag:0,microsecond:0,min:0,miss:0,mix:0,mode:0,modifi:0,modul:0,monochrom:0,more:0,most:0,mp4:0,multipl:0,must:0,name:0,nan:0,necessari:0,need:0,neg:0,no_autohint:0,no_bitmap:0,no_hint:0,no_recurs:0,no_scal:0,node:0,node_typ:0,non:0,none:0,normal:0,number:0,numpi:0,object:0,obtain:0,offici:0,offset:0,onc:0,one:0,onli:0,oper:0,option:0,order:0,orient:0,other:0,otherwis:0,out:0,out_filenam:0,out_fram:0,outgo:0,outlin:0,output:0,over:0,overlai:0,overlaid:0,overlay_parent_nod:0,overrid:0,overwrit:0,overwrite_output:0,pack:0,pad:0,page:0,pan:0,paramet:0,partial:0,pass:0,path:0,pcm:0,pedant:0,pipe:0,pipe_stderr:0,pipe_stdin:0,pipe_stdout:0,pipelin:0,pix_fmt:0,pixel:0,place:0,planar:0,pleas:0,point:0,popen:0,portion:0,posit:0,preced:0,present:0,preserv:0,probe:0,process1:0,process2:0,process:0,produc:0,properti:0,provid:0,pts:0,quiet:0,radian:0,rais:0,rand:0,random:0,rang:0,rate:0,ratio:0,rawvideo:0,read:0,reason:0,refer:0,rel:0,relat:0,reload:0,render:0,repeat:0,repeatlast:0,repres:0,represent:0,reshap:0,resolut:0,respect:0,result:0,retriev:0,revers:0,rgb24:0,rgb:0,right:0,run:0,run_async:0,same:0,sampl:0,san:0,sar:0,satur:0,search:0,second:0,secondari:0,section:0,see:0,segment:0,select:0,sent:0,separ:0,sequenc:0,set:0,setpt:0,shadow:0,shadowcolor:0,shadowi:0,shadowx:0,shape:0,shorter:0,shortest:0,shorthand:0,should:0,shown:0,silenc:0,singl:0,size:0,sloppi:0,some:0,space:0,special:0,specifi:0,split0:0,split1:0,split:0,stai:0,standard:0,start:0,start_fram:0,start_numb:0,start_pt:0,stderr:0,stdin:0,stdout:0,stream1:0,stream2:0,stream3:0,stream:0,stream_spec:0,streams_and_filenam:0,strftime:0,string:0,subpart:0,subprocess:0,subsampl:0,suffix:0,suppli:0,support:0,sure:0,synchron:0,synopsi:0,syntax:0,system:0,tab:0,tabsiz:0,take:0,taken:0,tc24hmax:0,tell:0,termin:0,text:0,text_h:0,text_shap:0,text_w:0,textfil:0,than:0,thei:0,them:0,thi:0,thick:0,through:0,thrown:0,time:0,timebas:0,timecod:0,timecode_r:0,timestamp:0,tobyt:0,togeth:0,top:0,track:0,tri:0,trim:0,tupl:0,type:0,uint8:0,unit:0,unknown:0,unsaf:0,until:0,updat:0,upper:0,upstream:0,upstream_label:0,upstream_nod:0,upstream_selector:0,upward:0,url:0,use:0,used:0,useful:0,user:0,uses:0,using:0,utf:0,util:0,valu:0,variabl:0,variou:0,vcodec:0,verbatim:0,vertic:0,vertical_layout:0,vflip:0,video:0,video_bitr:0,view:0,visibl:0,vsub:0,wai:0,wait:0,well:0,whatev:0,when:0,where:0,which:0,white:0,why:0,width:0,within:0,without:0,work:0,wrap:0,write:0,you:0,yuv420:0,yuv420p:0,yuv422:0,yuv422p:0,yuv444:0,zero:0,zoom:0,zoompan:0},titles:["ffmpeg-python: Python bindings for FFmpeg"],titleterms:{bind:0,ffmpeg:0,indic:0,python:0,tabl:0}}) \ No newline at end of file +Search.setIndex({docnames:["index"],envversion:{"sphinx.domains.c":1,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":1,"sphinx.domains.javascript":1,"sphinx.domains.math":2,"sphinx.domains.python":1,"sphinx.domains.rst":1,"sphinx.domains.std":1,sphinx:56},filenames:["index.rst"],objects:{"":{ffmpeg:[0,0,0,"-"]},"ffmpeg.Stream":{audio:[0,3,1,""],video:[0,3,1,""],view:[0,3,1,""]},ffmpeg:{Error:[0,1,1,""],Stream:[0,2,1,""],colorchannelmixer:[0,4,1,""],compile:[0,4,1,""],concat:[0,4,1,""],crop:[0,4,1,""],drawbox:[0,4,1,""],drawtext:[0,4,1,""],filter:[0,4,1,""],filter_:[0,4,1,""],filter_multi_output:[0,4,1,""],get_args:[0,4,1,""],hflip:[0,4,1,""],hue:[0,4,1,""],input:[0,4,1,""],merge_outputs:[0,4,1,""],output:[0,4,1,""],overlay:[0,4,1,""],overwrite_output:[0,4,1,""],probe:[0,4,1,""],run:[0,4,1,""],run_async:[0,4,1,""],setpts:[0,4,1,""],trim:[0,4,1,""],vflip:[0,4,1,""],view:[0,4,1,""],zoompan:[0,4,1,""]}},objnames:{"0":["py","module","Python module"],"1":["py","exception","Python exception"],"2":["py","class","Python class"],"3":["py","method","Python method"],"4":["py","function","Python function"]},objtypes:{"0":"py:module","1":"py:exception","2":"py:class","3":"py:method","4":"py:function"},terms:{"break":0,"case":0,"class":0,"default":0,"final":0,"function":0,"return":0,"true":0,"while":0,For:0,PTS:0,The:0,These:0,Used:0,accept:0,access:0,acodec:0,action:0,activ:0,adjust:0,aecho:0,after:0,alia:0,all:0,allow:0,alpha:0,also:0,altern:0,alwai:0,angl:0,ani:0,anoth:0,appli:0,arab:0,area:0,arg:0,argument:0,around:0,ascent:0,asetpt:0,ask:0,aspect:0,astyp:0,asynchron:0,atom:0,attempt:0,audio:0,audio_bitr:0,author:0,automat:0,avoid:0,axi:0,background:0,base:0,baselin:0,basetim:0,befor:0,behavior:0,behind:0,below:0,between:0,black:0,blend:0,border:0,bordercolor:0,borderw:0,both:0,box:0,boxborderw:0,boxcolor:0,bracket:0,bright:0,build:0,built:0,call:0,can:0,captur:0,capture_stderr:0,capture_stdout:0,care:0,certain:0,chang:0,channel:0,charact:0,check:0,child:0,chroma:0,clip:0,close:0,cmd:0,code:0,codec:0,collid:0,color:0,colorchannelmix:0,com:0,combin:0,command:0,commnad:0,common:0,commun:0,compil:0,concat:0,concaten:0,configur:0,confus:0,connect:0,constant:0,construct:0,consult:0,contain:0,continu:0,convert:0,coord:0,coordin:0,corner:0,correctli:0,correspond:0,count:0,creat:0,crop:0,crop_bitmap:0,custom:0,dar:0,data:0,debug:0,degre:0,deprec:0,descent:0,detail:0,dictionari:0,differ:0,dilemma:0,directli:0,disabl:0,displai:0,distanc:0,document:0,downstream:0,draw:0,drawbox:0,drawn:0,drawtext:0,drop:0,due:0,durat:0,dure:0,dynam:0,each:0,edg:0,effect:0,either:0,empti:0,emul:0,enabl:0,encod:0,encount:0,end:0,end_fram:0,end_pt:0,endal:0,eof:0,eof_act:0,equival:0,err:0,error:0,escape_text:0,etc:0,eval:0,evalu:0,even:0,exactli:0,exampl:0,except:0,exit:0,expand:0,expans:0,explicitli:0,expr:0,express:0,fail:0,fallback:0,fals:0,famili:0,ffmpeg_arg:0,ffprobe:0,file:0,filenam:0,filter:0,filter_:0,filter_multi_output:0,filter_nam:0,first:0,fix:0,fix_bound:0,flag:0,flip:0,follow:0,font:0,fontcolor:0,fontcolor_expr:0,fontconfig:0,fontfil:0,fontsiz:0,forc:0,force_autohint:0,format:0,fps:0,frame:0,frame_num:0,from:0,frombuff:0,ft_load_:0,ft_load_flag:0,gbrp:0,gener:0,get_arg:0,github:0,given:0,glyph:0,graph:0,greater:0,grid:0,handl:0,has:0,have:0,hd720:0,height:0,heigth:0,hflip:0,higher:0,highest:0,horizont:0,hour:0,how:0,hsub:0,http:0,hue:0,huge:0,ignore_global_advance_width:0,ignore_transform:0,imag:0,immedi:0,implement:0,in_byt:0,in_filenam:0,in_fram:0,includ:0,incom:0,independ:0,index:0,inform:0,init:0,initi:0,input:0,input_data:0,instead:0,interpret:0,intrins:0,invalid:0,invert:0,invok:0,its:0,join:0,json:0,just:0,kept:0,keyword:0,kkroen:0,kwarg:0,label:0,last:0,later:0,layout:0,left:0,level:0,libfontconfig:0,libfreetyp:0,libfribidi:0,librari:0,line:0,line_h:0,line_spac:0,linear_design:0,list:0,load:0,longest:0,lowest:0,luma:0,mai:0,main:0,main_h:0,main_parent_nod:0,main_w:0,mandatori:0,mani:0,manual:0,map:0,max:0,max_glyph_a:0,max_glyph_d:0,max_glyph_h:0,max_glyph_w:0,maximum:0,mean:0,merge_output:0,messag:0,microsecond:0,min:0,miss:0,mix:0,mode:0,modifi:0,modul:0,monochrom:0,more:0,most:0,mp4:0,multipl:0,must:0,name:0,nan:0,necessari:0,need:0,neg:0,no_autohint:0,no_bitmap:0,no_hint:0,no_recurs:0,no_scal:0,node:0,node_typ:0,non:0,none:0,normal:0,number:0,numpi:0,object:0,obtain:0,offici:0,offset:0,onc:0,one:0,onli:0,oper:0,option:0,order:0,orient:0,other:0,otherwis:0,out:0,out_filenam:0,out_fram:0,outgo:0,outlin:0,output:0,over:0,overlai:0,overlaid:0,overlay_parent_nod:0,overrid:0,overwrit:0,overwrite_output:0,pack:0,pad:0,page:0,pan:0,paramet:0,partial:0,pass:0,path:0,pcm:0,pedant:0,pipe:0,pipe_stderr:0,pipe_stdin:0,pipe_stdout:0,pipelin:0,pix_fmt:0,pixel:0,place:0,planar:0,pleas:0,point:0,popen:0,portion:0,posit:0,preced:0,present:0,preserv:0,probe:0,process1:0,process2:0,process:0,produc:0,properti:0,provid:0,pts:0,quiet:0,radian:0,rais:0,rand:0,random:0,rang:0,rate:0,ratio:0,rawvideo:0,read:0,reason:0,refer:0,rel:0,relat:0,reload:0,render:0,repeat:0,repeatlast:0,repres:0,represent:0,reshap:0,resolut:0,respect:0,result:0,retriev:0,revers:0,rgb24:0,rgb:0,right:0,run:0,run_async:0,same:0,sampl:0,san:0,sar:0,satur:0,search:0,second:0,secondari:0,section:0,see:0,segment:0,select:0,sent:0,separ:0,sequenc:0,set:0,setpt:0,shadow:0,shadowcolor:0,shadowi:0,shadowx:0,shape:0,shorter:0,shortest:0,shorthand:0,should:0,shown:0,silenc:0,singl:0,size:0,sloppi:0,some:0,space:0,special:0,specifi:0,split0:0,split1:0,split:0,stai:0,standard:0,start:0,start_fram:0,start_numb:0,start_pt:0,stderr:0,stdin:0,stdout:0,stream1:0,stream2:0,stream3:0,stream:0,stream_spec:0,streams_and_filenam:0,strftime:0,string:0,subpart:0,subprocess:0,subsampl:0,suffix:0,suppli:0,support:0,sure:0,synchron:0,synopsi:0,syntax:0,system:0,tab:0,tabsiz:0,take:0,taken:0,tc24hmax:0,tell:0,termin:0,text:0,text_h:0,text_shap:0,text_w:0,textfil:0,than:0,thei:0,them:0,thi:0,thick:0,through:0,thrown:0,time:0,timebas:0,timecod:0,timecode_r:0,timestamp:0,tobyt:0,togeth:0,top:0,track:0,tri:0,trim:0,tupl:0,type:0,uint8:0,unit:0,unknown:0,unsaf:0,until:0,updat:0,upper:0,upstream:0,upstream_label:0,upstream_nod:0,upstream_selector:0,upward:0,url:0,use:0,used:0,useful:0,user:0,uses:0,using:0,utf:0,util:0,valu:0,variabl:0,variou:0,vcodec:0,verbatim:0,vertic:0,vertical_layout:0,vflip:0,video:0,video_bitr:0,view:0,visibl:0,vsub:0,wai:0,wait:0,well:0,whatev:0,when:0,where:0,which:0,white:0,why:0,width:0,within:0,without:0,work:0,wrap:0,write:0,you:0,yuv420:0,yuv420p:0,yuv422:0,yuv422p:0,yuv444:0,zero:0,zoom:0,zoompan:0},titles:["ffmpeg-python: Python bindings for FFmpeg"],titleterms:{bind:0,ffmpeg:0,indic:0,python:0,tabl:0}}) diff --git a/doc/src/conf.py b/doc/src/conf.py index 48b11086..d5072992 100644 --- a/doc/src/conf.py +++ b/doc/src/conf.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # # ffmpeg-python documentation build configuration file, created by # sphinx-quickstart on Sat May 27 14:30:53 2017. @@ -18,6 +17,7 @@ # import os import sys + sys.path.insert(0, os.path.abspath('../..')) @@ -108,15 +108,12 @@ # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', - # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', - # Additional stuff for the LaTeX preamble. # # 'preamble': '', - # Latex figure (float) alignment # # 'figure_align': 'htbp', @@ -126,8 +123,7 @@ # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'ffmpeg-python.tex', u'ffmpeg-python Documentation', - u'Karl Kroening', 'manual'), + (master_doc, 'ffmpeg-python.tex', u'ffmpeg-python Documentation', u'Karl Kroening', 'manual'), ] @@ -135,10 +131,7 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'ffmpeg-python', u'ffmpeg-python Documentation', - [author], 1) -] +man_pages = [(master_doc, 'ffmpeg-python', u'ffmpeg-python Documentation', [author], 1)] # -- Options for Texinfo output ------------------------------------------- @@ -147,10 +140,13 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'ffmpeg-python', u'ffmpeg-python Documentation', - author, 'ffmpeg-python', 'One line description of project.', - 'Miscellaneous'), + ( + master_doc, + 'ffmpeg-python', + u'ffmpeg-python Documentation', + author, + 'ffmpeg-python', + 'One line description of project.', + 'Miscellaneous', + ), ] - - - diff --git a/examples/README.md b/examples/README.md index dabc7398..c3cf0f34 100644 --- a/examples/README.md +++ b/examples/README.md @@ -224,7 +224,7 @@ process = ( ffmpeg .input("input.mp4") .output( - server_url, + server_url, codec = "copy", # use same codecs of the original video listen=1, # enables HTTP server f=video_format) diff --git a/examples/facetime.py b/examples/facetime.py index 58d083ec..1ed14db1 100644 --- a/examples/facetime.py +++ b/examples/facetime.py @@ -1,8 +1,7 @@ import ffmpeg ( - ffmpeg - .input('FaceTime', format='avfoundation', pix_fmt='uyvy422', framerate=30) + ffmpeg.input('FaceTime', format='avfoundation', pix_fmt='uyvy422', framerate=30) .output('out.mp4', pix_fmt='yuv420p', vframes=100) .run() ) diff --git a/examples/ffmpeg-numpy.ipynb b/examples/ffmpeg-numpy.ipynb index b6d991bf..06a97ebe 100644 --- a/examples/ffmpeg-numpy.ipynb +++ b/examples/ffmpeg-numpy.ipynb @@ -1,216 +1,216 @@ { - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], - "source": [ - "from ipywidgets import interact\n", - "from matplotlib import pyplot as plt\n", - "import ffmpeg\n", - "import ipywidgets as widgets\n", - "import numpy as np" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [], - "source": [ - "probe = ffmpeg.probe('in.mp4')\n", - "video_info = next(s for s in probe['streams'] if s['codec_type'] == 'video')\n", - "width = int(video_info['width'])\n", - "height = int(video_info['height'])\n", - "num_frames = int(video_info['nb_frames'])" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ + "cells": [ { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "5f63dc164956464c994ec58d86ee7cd9", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "interactive(children=(IntSlider(value=0, description='frame', max=209), Output()), _dom_classes=('widget-inter…" + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from ipywidgets import interact\n", + "from matplotlib import pyplot as plt\n", + "import ffmpeg\n", + "import ipywidgets as widgets\n", + "import numpy as np" ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "out, err = (\n", - " ffmpeg\n", - " .input('in.mp4')\n", - " .output('pipe:', format='rawvideo', pix_fmt='rgb24')\n", - " .run(capture_stdout=True)\n", - ")\n", - "video = (\n", - " np\n", - " .frombuffer(out, np.uint8)\n", - " .reshape([-1, height, width, 3])\n", - ")\n", - "\n", - "@interact(frame=(0, num_frames))\n", - "def show_frame(frame=0):\n", - " plt.imshow(video[frame,:,:,:])" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ + }, { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "84bcac52195f47f8854f09acd7666b84", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "interactive(children=(Checkbox(value=True, description='enable_overlay'), Checkbox(value=True, description='en…" + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "probe = ffmpeg.probe('in.mp4')\n", + "video_info = next(s for s in probe['streams'] if s['codec_type'] == 'video')\n", + "width = int(video_info['width'])\n", + "height = int(video_info['height'])\n", + "num_frames = int(video_info['nb_frames'])" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "5f63dc164956464c994ec58d86ee7cd9", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "interactive(children=(IntSlider(value=0, description='frame', max=209), Output()), _dom_classes=('widget-inter\u2026" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "out, err = (\n", + " ffmpeg\n", + " .input('in.mp4')\n", + " .output('pipe:', format='rawvideo', pix_fmt='rgb24')\n", + " .run(capture_stdout=True)\n", + ")\n", + "video = (\n", + " np\n", + " .frombuffer(out, np.uint8)\n", + " .reshape([-1, height, width, 3])\n", + ")\n", + "\n", + "@interact(frame=(0, num_frames))\n", + "def show_frame(frame=0):\n", + " plt.imshow(video[frame,:,:,:])" ] - }, - "metadata": {}, - "output_type": "display_data" + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "84bcac52195f47f8854f09acd7666b84", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "interactive(children=(Checkbox(value=True, description='enable_overlay'), Checkbox(value=True, description='en\u2026" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from io import BytesIO\n", + "from PIL import Image\n", + "\n", + "\n", + "def extract_frame(stream, frame_num):\n", + " while isinstance(stream, ffmpeg.nodes.OutputStream):\n", + " stream = stream.node.incoming_edges[0].upstream_node.stream()\n", + " out, _ = (\n", + " stream\n", + " .filter_('select', 'gte(n,{})'.format(frame_num))\n", + " .output('pipe:', format='rawvideo', pix_fmt='rgb24', vframes=1)\n", + " .run(capture_stdout=True, capture_stderr=True)\n", + " )\n", + " return np.frombuffer(out, np.uint8).reshape([height, width, 3])\n", + "\n", + "\n", + "def png_to_np(png_bytes):\n", + " buffer = BytesIO(png_bytes)\n", + " pil_image = Image.open(buffer)\n", + " return np.array(pil_image)\n", + " \n", + "\n", + "def build_graph(\n", + " enable_overlay, flip_overlay, enable_box, box_x, box_y,\n", + " thickness, color):\n", + "\n", + " stream = ffmpeg.input('in.mp4')\n", + "\n", + " if enable_overlay:\n", + " overlay = ffmpeg.input('overlay.png')\n", + " if flip_overlay:\n", + " overlay = overlay.hflip()\n", + " stream = stream.overlay(overlay)\n", + "\n", + " if enable_box:\n", + " stream = stream.drawbox(\n", + " box_x, box_y, 120, 120, color=color, t=thickness)\n", + "\n", + " return stream.output('out.mp4')\n", + "\n", + "\n", + "def show_image(ax, stream, frame_num):\n", + " try:\n", + " image = extract_frame(stream, frame_num)\n", + " ax.imshow(image)\n", + " ax.axis('off')\n", + " except ffmpeg.Error as e:\n", + " print(e.stderr.decode())\n", + "\n", + "\n", + "def show_graph(ax, stream, detail):\n", + " data = ffmpeg.view(stream, detail=detail, pipe=True)\n", + " image = png_to_np(data)\n", + " ax.imshow(image, aspect='equal', interpolation='hanning')\n", + " ax.set_xlim(0, 1100)\n", + " ax.axis('off')\n", + "\n", + "\n", + "@interact(\n", + " frame_num=(0, num_frames),\n", + " box_x=(0, 200),\n", + " box_y=(0, 200),\n", + " thickness=(1, 40),\n", + " color=['red', 'green', 'magenta', 'blue'],\n", + ")\n", + "def f(\n", + " enable_overlay=True,\n", + " enable_box=True,\n", + " flip_overlay=True,\n", + " graph_detail=False,\n", + " frame_num=0,\n", + " box_x=50,\n", + " box_y=50,\n", + " thickness=5,\n", + " color='red'):\n", + "\n", + " stream = build_graph(\n", + " enable_overlay,\n", + " flip_overlay,\n", + " enable_box,\n", + " box_x,\n", + " box_y,\n", + " thickness,\n", + " color\n", + " )\n", + "\n", + " fig, (ax0, ax1) = plt.subplots(1, 2, figsize=(15,4))\n", + " plt.tight_layout()\n", + " show_image(ax0, stream, frame_num)\n", + " show_graph(ax1, stream, graph_detail)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.4" } - ], - "source": [ - "from io import BytesIO\n", - "from PIL import Image\n", - "\n", - "\n", - "def extract_frame(stream, frame_num):\n", - " while isinstance(stream, ffmpeg.nodes.OutputStream):\n", - " stream = stream.node.incoming_edges[0].upstream_node.stream()\n", - " out, _ = (\n", - " stream\n", - " .filter_('select', 'gte(n,{})'.format(frame_num))\n", - " .output('pipe:', format='rawvideo', pix_fmt='rgb24', vframes=1)\n", - " .run(capture_stdout=True, capture_stderr=True)\n", - " )\n", - " return np.frombuffer(out, np.uint8).reshape([height, width, 3])\n", - "\n", - "\n", - "def png_to_np(png_bytes):\n", - " buffer = BytesIO(png_bytes)\n", - " pil_image = Image.open(buffer)\n", - " return np.array(pil_image)\n", - " \n", - "\n", - "def build_graph(\n", - " enable_overlay, flip_overlay, enable_box, box_x, box_y,\n", - " thickness, color):\n", - "\n", - " stream = ffmpeg.input('in.mp4')\n", - "\n", - " if enable_overlay:\n", - " overlay = ffmpeg.input('overlay.png')\n", - " if flip_overlay:\n", - " overlay = overlay.hflip()\n", - " stream = stream.overlay(overlay)\n", - "\n", - " if enable_box:\n", - " stream = stream.drawbox(\n", - " box_x, box_y, 120, 120, color=color, t=thickness)\n", - "\n", - " return stream.output('out.mp4')\n", - "\n", - "\n", - "def show_image(ax, stream, frame_num):\n", - " try:\n", - " image = extract_frame(stream, frame_num)\n", - " ax.imshow(image)\n", - " ax.axis('off')\n", - " except ffmpeg.Error as e:\n", - " print(e.stderr.decode())\n", - "\n", - "\n", - "def show_graph(ax, stream, detail):\n", - " data = ffmpeg.view(stream, detail=detail, pipe=True)\n", - " image = png_to_np(data)\n", - " ax.imshow(image, aspect='equal', interpolation='hanning')\n", - " ax.set_xlim(0, 1100)\n", - " ax.axis('off')\n", - "\n", - "\n", - "@interact(\n", - " frame_num=(0, num_frames),\n", - " box_x=(0, 200),\n", - " box_y=(0, 200),\n", - " thickness=(1, 40),\n", - " color=['red', 'green', 'magenta', 'blue'],\n", - ")\n", - "def f(\n", - " enable_overlay=True,\n", - " enable_box=True,\n", - " flip_overlay=True,\n", - " graph_detail=False,\n", - " frame_num=0,\n", - " box_x=50,\n", - " box_y=50,\n", - " thickness=5,\n", - " color='red'):\n", - "\n", - " stream = build_graph(\n", - " enable_overlay,\n", - " flip_overlay,\n", - " enable_box,\n", - " box_x,\n", - " box_y,\n", - " thickness,\n", - " color\n", - " )\n", - "\n", - " fig, (ax0, ax1) = plt.subplots(1, 2, figsize=(15,4))\n", - " plt.tight_layout()\n", - " show_image(ax0, stream, frame_num)\n", - " show_graph(ax1, stream, graph_detail)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.6.4" - } - }, - "nbformat": 4, - "nbformat_minor": 2 + "nbformat": 4, + "nbformat_minor": 2 } diff --git a/examples/get_video_thumbnail.py b/examples/get_video_thumbnail.py index b905642f..a3d994cd 100755 --- a/examples/get_video_thumbnail.py +++ b/examples/get_video_thumbnail.py @@ -1,25 +1,24 @@ #!/usr/bin/env python -from __future__ import unicode_literals, print_function +from __future__ import print_function, unicode_literals + import argparse -import ffmpeg import sys +import ffmpeg parser = argparse.ArgumentParser(description='Generate video thumbnail') parser.add_argument('in_filename', help='Input filename') parser.add_argument('out_filename', help='Output filename') +parser.add_argument('--time', type=int, default=0.1, help='Time offset') parser.add_argument( - '--time', type=int, default=0.1, help='Time offset') -parser.add_argument( - '--width', type=int, default=120, - help='Width of output thumbnail (height automatically determined by aspect ratio)') + '--width', type=int, default=120, help='Width of output thumbnail (height automatically determined by aspect ratio)' +) def generate_thumbnail(in_filename, out_filename, time, width): try: ( - ffmpeg - .input(in_filename, ss=time) + ffmpeg.input(in_filename, ss=time) .filter('scale', width, -1) .output(out_filename, vframes=1) .overwrite_output() diff --git a/examples/read_frame_as_jpeg.py b/examples/read_frame_as_jpeg.py index 92b4feec..a17d6099 100755 --- a/examples/read_frame_as_jpeg.py +++ b/examples/read_frame_as_jpeg.py @@ -1,20 +1,19 @@ #!/usr/bin/env python from __future__ import unicode_literals + import argparse -import ffmpeg import sys +import ffmpeg -parser = argparse.ArgumentParser( - description='Read individual video frame into memory as jpeg and write to stdout') +parser = argparse.ArgumentParser(description='Read individual video frame into memory as jpeg and write to stdout') parser.add_argument('in_filename', help='Input filename') parser.add_argument('frame_num', help='Frame number') def read_frame_as_jpeg(in_filename, frame_num): out, err = ( - ffmpeg - .input(in_filename) + ffmpeg.input(in_filename) .filter('select', 'gte(n,{})'.format(frame_num)) .output('pipe:', vframes=1, format='image2', vcodec='mjpeg') .run(capture_stdout=True) diff --git a/examples/show_progress.py b/examples/show_progress.py index dd0253a1..bd390a92 100755 --- a/examples/show_progress.py +++ b/examples/show_progress.py @@ -1,11 +1,8 @@ #!/usr/bin/env python -from __future__ import unicode_literals, print_function -from tqdm import tqdm +from __future__ import print_function, unicode_literals + import argparse import contextlib -import ffmpeg -import gevent -import gevent.monkey; gevent.monkey.patch_all(thread=False) import os import shutil import socket @@ -13,8 +10,18 @@ import tempfile import textwrap +import gevent +import gevent.monkey +from tqdm import tqdm + +import ffmpeg + +gevent.monkey.patch_all(thread=False) -parser = argparse.ArgumentParser(description=textwrap.dedent('''\ + +parser = argparse.ArgumentParser( + description=textwrap.dedent( + '''\ Process video and report and show progress bar. This is an example of using the ffmpeg `-progress` option with a @@ -24,7 +31,9 @@ The video processing simply consists of converting the video to sepia colors, but the same pattern can be applied to other use cases. -''')) +''' + ) +) parser.add_argument('in_filename', help='Input filename') parser.add_argument('out_filename', help='Output filename') @@ -92,18 +101,19 @@ def _watch_progress(handler): raise - @contextlib.contextmanager def show_progress(total_duration): """Create a unix-domain socket to watch progress and render tqdm progress bar.""" with tqdm(total=round(total_duration, 2)) as bar: + def handler(key, value): if key == 'out_time_ms': - time = round(float(value) / 1000000., 2) + time = round(float(value) / 1000000.0, 2) bar.update(time - bar.n) elif key == 'progress' and value == 'end': bar.update(bar.total - bar.n) + with _watch_progress(handler) as socket_filename: yield socket_filename @@ -114,10 +124,10 @@ def handler(key, value): with show_progress(total_duration) as socket_filename: # See https://ffmpeg.org/ffmpeg-filters.html#Examples-44 - sepia_values = [.393, .769, .189, 0, .349, .686, .168, 0, .272, .534, .131] + sepia_values = [0.393, 0.769, 0.189, 0, 0.349, 0.686, 0.168, 0, 0.272, 0.534, 0.131] try: - (ffmpeg - .input(args.in_filename) + ( + ffmpeg.input(args.in_filename) .colorchannelmixer(*sepia_values) .output(args.out_filename) .global_args('-progress', 'unix://{}'.format(socket_filename)) @@ -127,4 +137,3 @@ def handler(key, value): except ffmpeg.Error as e: print(e.stderr, file=sys.stderr) sys.exit(1) - diff --git a/examples/split_silence.py b/examples/split_silence.py index 90b46d95..3e57fb10 100755 --- a/examples/split_silence.py +++ b/examples/split_silence.py @@ -3,13 +3,13 @@ import argparse import errno -import ffmpeg import logging import os import re import subprocess import sys +import ffmpeg logging.basicConfig(level=logging.INFO, format='%(message)s') logger = logging.getLogger(__file__) @@ -30,7 +30,8 @@ silence_start_re = re.compile(r' silence_start: (?P[0-9]+(\.?[0-9]*))$') silence_end_re = re.compile(r' silence_end: (?P[0-9]+(\.?[0-9]*)) ') total_duration_re = re.compile( - r'size=[^ ]+ time=(?P[0-9]{2}):(?P[0-9]{2}):(?P[0-9\.]{5}) bitrate=') + r'size=[^ ]+ time=(?P[0-9]{2}):(?P[0-9]{2}):(?P[0-9\.]{5}) bitrate=' +) def _logged_popen(cmd_line, *args, **kwargs): @@ -43,18 +44,19 @@ def get_chunk_times(in_filename, silence_threshold, silence_duration, start_time if start_time is not None: input_kwargs['ss'] = start_time else: - start_time = 0. + start_time = 0.0 if end_time is not None: input_kwargs['t'] = end_time - start_time p = _logged_popen( - (ffmpeg - .input(in_filename, **input_kwargs) + ( + ffmpeg.input(in_filename, **input_kwargs) .filter('silencedetect', n='{}dB'.format(silence_threshold), d=silence_duration) .output('-', format='null') .compile() - ) + ['-nostats'], # FIXME: use .nostats() once it's implemented in ffmpeg-python. - stderr=subprocess.PIPE + ) + + ['-nostats'], # FIXME: use .nostats() once it's implemented in ffmpeg-python. + stderr=subprocess.PIPE, ) output = p.communicate()[1].decode('utf-8') if p.returncode != 0: @@ -74,7 +76,7 @@ def get_chunk_times(in_filename, silence_threshold, silence_duration, start_time chunk_ends.append(float(silence_start_match.group('start'))) if len(chunk_starts) == 0: # Started with non-silence. - chunk_starts.append(start_time or 0.) + chunk_starts.append(start_time or 0.0) elif silence_end_match: chunk_starts.append(float(silence_end_match.group('end'))) elif total_duration_match: @@ -89,7 +91,7 @@ def get_chunk_times(in_filename, silence_threshold, silence_duration, start_time if len(chunk_starts) > len(chunk_ends): # Finished with non-silence. - chunk_ends.append(end_time or 10000000.) + chunk_ends.append(end_time or 10000000.0) return list(zip(chunk_starts, chunk_ends)) @@ -119,15 +121,9 @@ def split_audio( out_filename = out_pattern.format(i, i=i) _makedirs(os.path.dirname(out_filename)) - logger.info('{}: start={:.02f}, end={:.02f}, duration={:.02f}'.format(out_filename, start_time, end_time, - time)) + logger.info('{}: start={:.02f}, end={:.02f}, duration={:.02f}'.format(out_filename, start_time, end_time, time)) _logged_popen( - (ffmpeg - .input(in_filename, ss=start_time, t=time) - .output(out_filename) - .overwrite_output() - .compile() - ), + (ffmpeg.input(in_filename, ss=start_time, t=time).output(out_filename).overwrite_output().compile()), stdout=subprocess.PIPE if not verbose else None, stderr=subprocess.PIPE if not verbose else None, ).communicate() diff --git a/examples/tensorflow_stream.py b/examples/tensorflow_stream.py index 6c9c9c9d..8811c692 100644 --- a/examples/tensorflow_stream.py +++ b/examples/tensorflow_stream.py @@ -25,20 +25,21 @@ is installed before running) ''' from __future__ import print_function + import argparse -import ffmpeg import logging -import numpy as np import os import subprocess import zipfile +import numpy as np + +import ffmpeg parser = argparse.ArgumentParser(description='Example streaming ffmpeg numpy processing') parser.add_argument('in_filename', help='Input filename') parser.add_argument('out_filename', help='Output filename') -parser.add_argument( - '--dream', action='store_true', help='Use DeepDream frame processing (requires tensorflow)') +parser.add_argument('--dream', action='store_true', help='Use DeepDream frame processing (requires tensorflow)') logger = logging.getLogger(__name__) logging.basicConfig(level=logging.INFO) @@ -55,20 +56,14 @@ def get_video_size(filename): def start_ffmpeg_process1(in_filename): logger.info('Starting ffmpeg process1') - args = ( - ffmpeg - .input(in_filename) - .output('pipe:', format='rawvideo', pix_fmt='rgb24') - .compile() - ) + args = ffmpeg.input(in_filename).output('pipe:', format='rawvideo', pix_fmt='rgb24').compile() return subprocess.Popen(args, stdout=subprocess.PIPE) def start_ffmpeg_process2(out_filename, width, height): logger.info('Starting ffmpeg process2') args = ( - ffmpeg - .input('pipe:', format='rawvideo', pix_fmt='rgb24', s='{}x{}'.format(width, height)) + ffmpeg.input('pipe:', format='rawvideo', pix_fmt='rgb24', s='{}x{}'.format(width, height)) .output(out_filename, pix_fmt='yuv420p') .overwrite_output() .compile() @@ -86,11 +81,7 @@ def read_frame(process1, width, height): frame = None else: assert len(in_bytes) == frame_size - frame = ( - np - .frombuffer(in_bytes, np.uint8) - .reshape([height, width, 3]) - ) + frame = np.frombuffer(in_bytes, np.uint8).reshape([height, width, 3]) return frame @@ -101,11 +92,7 @@ def process_frame_simple(frame): def write_frame(process2, frame): logger.debug('Writing frame') - process2.stdin.write( - frame - .astype(np.uint8) - .tobytes() - ) + process2.stdin.write(frame.astype(np.uint8).tobytes()) def run(in_filename, out_filename, process_frame): @@ -161,18 +148,22 @@ def _tffunc(*argtypes): See `_resize` function below. ''' placeholders = list(map(tf.placeholder, argtypes)) + def wrap(f): out = f(*placeholders) + def wrapper(*args, **kw): return out.eval(dict(zip(placeholders, args)), session=kw.get('session')) + return wrapper + return wrap @staticmethod def _base_resize(img, size): '''Helper function that uses TF to resize an image''' img = tf.expand_dims(img, 0) - return tf.image.resize_bilinear(img, size)[0,:,:,:] + return tf.image.resize_bilinear(img, size)[0, :, :, :] def __init__(self): if not os.path.exists(DeepDream._MODEL_FILENAME): @@ -184,57 +175,57 @@ def __init__(self): with tf.gfile.FastGFile(DeepDream._MODEL_FILENAME, 'rb') as f: graph_def = tf.GraphDef() graph_def.ParseFromString(f.read()) - self._t_input = tf.placeholder(np.float32, name='input') # define the input tensor + self._t_input = tf.placeholder(np.float32, name='input') # define the input tensor imagenet_mean = 117.0 - t_preprocessed = tf.expand_dims(self._t_input-imagenet_mean, 0) - tf.import_graph_def(graph_def, {'input':t_preprocessed}) + t_preprocessed = tf.expand_dims(self._t_input - imagenet_mean, 0) + tf.import_graph_def(graph_def, {'input': t_preprocessed}) - self.t_obj = self.T('mixed4d_3x3_bottleneck_pre_relu')[:,:,:,139] - #self.t_obj = tf.square(self.T('mixed4c')) + self.t_obj = self.T('mixed4d_3x3_bottleneck_pre_relu')[:, :, :, 139] + # self.t_obj = tf.square(self.T('mixed4c')) def T(self, layer_name): '''Helper for getting layer output tensor''' - return self._graph.get_tensor_by_name('import/%s:0'%layer_name) + return self._graph.get_tensor_by_name('import/%s:0' % layer_name) def _calc_grad_tiled(self, img, t_grad, tile_size=512): '''Compute the value of tensor t_grad over the image in a tiled way. - Random shifts are applied to the image to blur tile boundaries over + Random shifts are applied to the image to blur tile boundaries over multiple iterations.''' sz = tile_size h, w = img.shape[:2] sx, sy = np.random.randint(sz, size=2) img_shift = np.roll(np.roll(img, sx, 1), sy, 0) grad = np.zeros_like(img) - for y in range(0, max(h-sz//2, sz),sz): - for x in range(0, max(w-sz//2, sz),sz): - sub = img_shift[y:y+sz,x:x+sz] - g = self._session.run(t_grad, {self._t_input:sub}) - grad[y:y+sz,x:x+sz] = g + for y in range(0, max(h - sz // 2, sz), sz): + for x in range(0, max(w - sz // 2, sz), sz): + sub = img_shift[y : y + sz, x : x + sz] + g = self._session.run(t_grad, {self._t_input: sub}) + grad[y : y + sz, x : x + sz] = g return np.roll(np.roll(grad, -sx, 1), -sy, 0) def process_frame(self, frame, iter_n=10, step=1.5, octave_n=4, octave_scale=1.4): - t_score = tf.reduce_mean(self.t_obj) # defining the optimization objective - t_grad = tf.gradients(t_score, self._t_input)[0] # behold the power of automatic differentiation! + t_score = tf.reduce_mean(self.t_obj) # defining the optimization objective + t_grad = tf.gradients(t_score, self._t_input)[0] # behold the power of automatic differentiation! # split the image into a number of octaves img = frame octaves = [] - for i in range(octave_n-1): + for i in range(octave_n - 1): hw = img.shape[:2] - lo = self._resize(img, np.int32(np.float32(hw)/octave_scale)) - hi = img-self._resize(lo, hw) + lo = self._resize(img, np.int32(np.float32(hw) / octave_scale)) + hi = img - self._resize(lo, hw) img = lo octaves.append(hi) - + # generate details octave by octave for octave in range(octave_n): - if octave>0: + if octave > 0: hi = octaves[-octave] - img = self._resize(img, hi.shape[:2])+hi + img = self._resize(img, hi.shape[:2]) + hi for i in range(iter_n): g = self._calc_grad_tiled(img, t_grad) - img += g*(step / (np.abs(g).mean()+1e-7)) - #print('.',end = ' ') + img += g * (step / (np.abs(g).mean() + 1e-7)) + # print('.',end = ' ') return img @@ -242,6 +233,7 @@ def process_frame(self, frame, iter_n=10, step=1.5, octave_n=4, octave_scale=1.4 args = parser.parse_args() if args.dream: import tensorflow as tf + process_frame = DeepDream().process_frame else: process_frame = process_frame_simple diff --git a/examples/transcribe.py b/examples/transcribe.py index 0b7200c4..226b0490 100755 --- a/examples/transcribe.py +++ b/examples/transcribe.py @@ -1,13 +1,14 @@ #!/usr/bin/env python -from __future__ import unicode_literals, print_function -from google.cloud import speech -from google.cloud.speech import enums -from google.cloud.speech import types +from __future__ import print_function, unicode_literals + import argparse -import ffmpeg import logging import sys +from google.cloud import speech +from google.cloud.speech import enums, types + +import ffmpeg logging.basicConfig(level=logging.INFO, format='%(message)s') logger = logging.getLogger(__file__) @@ -20,8 +21,8 @@ def decode_audio(in_filename, **input_kwargs): try: - out, err = (ffmpeg - .input(in_filename, **input_kwargs) + out, err = ( + ffmpeg.input(in_filename, **input_kwargs) .output('-', format='s16le', acodec='pcm_s16le', ac=1, ar='16k') .overwrite_output() .run(capture_stdout=True, capture_stderr=True) @@ -36,9 +37,7 @@ def get_transcripts(audio_data): client = speech.SpeechClient() audio = types.RecognitionAudio(content=audio_data) config = types.RecognitionConfig( - encoding=enums.RecognitionConfig.AudioEncoding.LINEAR16, - sample_rate_hertz=16000, - language_code='en-US' + encoding=enums.RecognitionConfig.AudioEncoding.LINEAR16, sample_rate_hertz=16000, language_code='en-US' ) response = client.recognize(config, audio) return [result.alternatives[0].transcript for result in response.results] diff --git a/examples/video_info.py b/examples/video_info.py index df9c992e..9dd96b40 100755 --- a/examples/video_info.py +++ b/examples/video_info.py @@ -1,9 +1,10 @@ #!/usr/bin/env python -from __future__ import unicode_literals, print_function +from __future__ import print_function, unicode_literals + import argparse -import ffmpeg import sys +import ffmpeg parser = argparse.ArgumentParser(description='Get video information') parser.add_argument('in_filename', help='Input filename') diff --git a/ffmpeg/__init__.py b/ffmpeg/__init__.py index a88d344d..be31a179 100644 --- a/ffmpeg/__init__.py +++ b/ffmpeg/__init__.py @@ -1,22 +1,5 @@ from __future__ import unicode_literals -from . import nodes -from . import _ffmpeg -from . import _filters -from . import _probe -from . import _run -from . import _view -from .nodes import * -from ._ffmpeg import * -from ._filters import * -from ._probe import * -from ._run import * -from ._view import * -__all__ = ( - nodes.__all__ - + _ffmpeg.__all__ - + _probe.__all__ - + _run.__all__ - + _view.__all__ - + _filters.__all__ -) +from . import _ffmpeg, _filters, _probe, _run, _view, nodes + +__all__ = nodes.__all__ + _ffmpeg.__all__ + _probe.__all__ + _run.__all__ + _view.__all__ + _filters.__all__ diff --git a/ffmpeg/_ffmpeg.py b/ffmpeg/_ffmpeg.py index 007624bb..c888a762 100644 --- a/ffmpeg/_ffmpeg.py +++ b/ffmpeg/_ffmpeg.py @@ -1,19 +1,21 @@ from __future__ import unicode_literals -from past.builtins import basestring -from ._utils import basestring +from typing import Any from .nodes import ( - filter_operator, + FilterableStream, GlobalNode, InputNode, MergeOutputsNode, OutputNode, + OutputStream, + Stream, + filter_operator, output_operator, ) -def input(filename, **kwargs): +def input(filename: str, **kwargs: Any) -> Stream: """Input file URL (ffmpeg ``-i`` option) Any supplied kwargs are passed to ffmpeg verbatim (e.g. ``t=20``, @@ -33,13 +35,13 @@ def input(filename, **kwargs): @output_operator() -def global_args(stream, *args): +def global_args(stream: OutputStream, *args: str) -> Stream: """Add extra global command-line argument(s), e.g. ``-progress``.""" return GlobalNode(stream, global_args.__name__, args).stream() @output_operator() -def overwrite_output(stream): +def overwrite_output(stream: OutputStream) -> Stream: """Overwrite output files without asking (ffmpeg ``-y`` option) Official documentation: `Main options `__ @@ -48,13 +50,13 @@ def overwrite_output(stream): @output_operator() -def merge_outputs(*streams): +def merge_outputs(*streams: OutputStream) -> Stream: """Include all given outputs in one ffmpeg command line""" return MergeOutputsNode(streams, merge_outputs.__name__).stream() @filter_operator() -def output(*streams_and_filename, **kwargs): +def output(*streams_and_filename_: str | FilterableStream, **kwargs: Any) -> OutputStream: """Output file URL Syntax: @@ -77,9 +79,9 @@ def output(*streams_and_filename, **kwargs): Official documentation: `Synopsis `__ """ - streams_and_filename = list(streams_and_filename) + streams_and_filename = list(streams_and_filename_) if 'filename' not in kwargs: - if not isinstance(streams_and_filename[-1], basestring): + if not isinstance(streams_and_filename[-1], str): raise ValueError('A filename must be provided') kwargs['filename'] = streams_and_filename.pop(-1) streams = streams_and_filename @@ -89,7 +91,9 @@ def output(*streams_and_filename, **kwargs): if 'format' in kwargs: raise ValueError("Can't specify both `format` and `f` kwargs") kwargs['format'] = fmt - return OutputNode(streams, output.__name__, kwargs=kwargs).stream() + stream = OutputNode(tuple(k for k in streams if isinstance(k, Stream)), output.__name__, kwargs=kwargs).stream() + assert isinstance(stream, OutputStream) + return stream __all__ = ['input', 'merge_outputs', 'output', 'overwrite_output'] diff --git a/ffmpeg/_filters.py b/ffmpeg/_filters.py index 5bca23d8..2980366a 100644 --- a/ffmpeg/_filters.py +++ b/ffmpeg/_filters.py @@ -1,7 +1,7 @@ from __future__ import unicode_literals -from .nodes import FilterNode, filter_operator from ._utils import escape_chars +from .nodes import FilterNode, filter_operator @filter_operator() @@ -23,9 +23,7 @@ def filter_multi_output(stream_spec, filter_name, *args, **kwargs): ffmpeg.concat(split0, split1).output('out.mp4').run() ``` """ - return FilterNode( - stream_spec, filter_name, args=args, kwargs=kwargs, max_inputs=None - ) + return FilterNode(stream_spec, filter_name, args=args, kwargs=kwargs, max_inputs=None) @filter_operator() @@ -197,9 +195,7 @@ def crop(stream, x, y, width, height, **kwargs): Official documentation: `crop `__ """ - return FilterNode( - stream, crop.__name__, args=[width, height, x, y], kwargs=kwargs - ).stream() + return FilterNode(stream, crop.__name__, args=[width, height, x, y], kwargs=kwargs).stream() @filter_operator() @@ -230,9 +226,7 @@ def drawbox(stream, x, y, width, height, color, thickness=None, **kwargs): """ if thickness: kwargs['t'] = thickness - return FilterNode( - stream, drawbox.__name__, args=[x, y, width, height, color], kwargs=kwargs - ).stream() + return FilterNode(stream, drawbox.__name__, args=[x, y, width, height, color], kwargs=kwargs).stream() @filter_operator() diff --git a/ffmpeg/_probe.py b/ffmpeg/_probe.py index 090d7abf..ff09e49c 100644 --- a/ffmpeg/_probe.py +++ b/ffmpeg/_probe.py @@ -1,5 +1,6 @@ import json import subprocess + from ._run import Error from ._utils import convert_kwargs_to_cmd_line_args diff --git a/ffmpeg/_run.py b/ffmpeg/_run.py index f42d1d73..8e1bbcab 100644 --- a/ffmpeg/_run.py +++ b/ffmpeg/_run.py @@ -1,47 +1,35 @@ from __future__ import unicode_literals -from .dag import get_outgoing_edges, topo_sort -from ._utils import basestring, convert_kwargs_to_cmd_line_args -from builtins import str -from functools import reduce + import copy import operator import subprocess +from collections.abc import Iterable +from functools import reduce from ._ffmpeg import input, output -from .nodes import ( - get_stream_spec_nodes, - FilterNode, - GlobalNode, - InputNode, - OutputNode, - output_operator, -) - -try: - from collections.abc import Iterable -except ImportError: - from collections import Iterable +from ._utils import convert_kwargs_to_cmd_line_args +from .dag import DagEdge, KwargReprNode, OutgoingEdgeMap, get_outgoing_edges, topo_sort +from .nodes import FilterNode, GlobalNode, InputNode, OutputNode, OutputStream, get_stream_spec_nodes, output_operator class Error(Exception): - def __init__(self, cmd, stdout, stderr): - super(Error, self).__init__( - '{} error (see stderr output for detail)'.format(cmd) - ) + def __init__(self, cmd: list[str] | str, stdout: str, stderr: str): + super(Error, self).__init__('{} error (see stderr output for detail)'.format(cmd)) self.stdout = stdout self.stderr = stderr -def _get_input_args(input_node): +def _get_input_args(input_node: InputNode) -> list[str]: if input_node.name == input.__name__: kwargs = copy.copy(input_node.kwargs) - filename = kwargs.pop('filename') + filename = str(kwargs.pop('filename')) fmt = kwargs.pop('format', None) video_size = kwargs.pop('video_size', None) args = [] if fmt: - args += ['-f', fmt] + args += ['-f', str(fmt)] if video_size: + assert isinstance(video_size, Iterable) args += ['-video_size', '{}x{}'.format(video_size[0], video_size[1])] args += convert_kwargs_to_cmd_line_args(kwargs) args += ['-i', filename] @@ -50,7 +38,10 @@ def _get_input_args(input_node): return args -def _format_input_stream_name(stream_name_map, edge, is_final_arg=False): +StreamNameMap = dict[tuple[KwargReprNode, None | int], str] + + +def _format_input_stream_name(stream_name_map: StreamNameMap, edge: DagEdge, is_final_arg: bool = False) -> str: prefix = stream_name_map[edge.upstream_node, edge.upstream_label] if not edge.upstream_selector: suffix = '' @@ -65,26 +56,28 @@ def _format_input_stream_name(stream_name_map, edge, is_final_arg=False): return fmt.format(prefix, suffix) -def _format_output_stream_name(stream_name_map, edge): +def _format_output_stream_name(stream_name_map: StreamNameMap, edge: DagEdge) -> str: return '[{}]'.format(stream_name_map[edge.upstream_node, edge.upstream_label]) -def _get_filter_spec(node, outgoing_edge_map, stream_name_map): +def _get_filter_spec( + node: FilterNode, + outgoing_edge_map: OutgoingEdgeMap, + stream_name_map: StreamNameMap, +) -> str: incoming_edges = node.incoming_edges outgoing_edges = get_outgoing_edges(node, outgoing_edge_map) - inputs = [ - _format_input_stream_name(stream_name_map, edge) for edge in incoming_edges - ] - outputs = [ - _format_output_stream_name(stream_name_map, edge) for edge in outgoing_edges - ] - filter_spec = '{}{}{}'.format( - ''.join(inputs), node._get_filter(outgoing_edges), ''.join(outputs) - ) + inputs = [_format_input_stream_name(stream_name_map, edge) for edge in incoming_edges] + outputs = [_format_output_stream_name(stream_name_map, edge) for edge in outgoing_edges] + filter_spec = '{}{}{}'.format(''.join(inputs), node._get_filter(outgoing_edges), ''.join(outputs)) return filter_spec -def _allocate_filter_stream_names(filter_nodes, outgoing_edge_maps, stream_name_map): +def _allocate_filter_stream_names( + filter_nodes: list[FilterNode], + outgoing_edge_maps: dict[KwargReprNode, OutgoingEdgeMap], + stream_name_map: StreamNameMap, +) -> None: stream_count = 0 for upstream_node in filter_nodes: outgoing_edge_map = outgoing_edge_maps[upstream_node] @@ -93,54 +86,54 @@ def _allocate_filter_stream_names(filter_nodes, outgoing_edge_maps, stream_name_ # TODO: automatically insert `splits` ahead of time via graph transformation. raise ValueError( 'Encountered {} with multiple outgoing edges with same upstream ' - 'label {!r}; a `split` filter is probably required'.format( - upstream_node, upstream_label - ) + 'label {!r}; a `split` filter is probably required'.format(upstream_node, upstream_label) ) stream_name_map[upstream_node, upstream_label] = 's{}'.format(stream_count) stream_count += 1 -def _get_filter_arg(filter_nodes, outgoing_edge_maps, stream_name_map): +def _get_filter_arg( + filter_nodes: list[FilterNode], + outgoing_edge_maps: dict[KwargReprNode, OutgoingEdgeMap], + stream_name_map: StreamNameMap, +) -> str: _allocate_filter_stream_names(filter_nodes, outgoing_edge_maps, stream_name_map) - filter_specs = [ - _get_filter_spec(node, outgoing_edge_maps[node], stream_name_map) - for node in filter_nodes - ] + filter_specs = [_get_filter_spec(node, outgoing_edge_maps[node], stream_name_map) for node in filter_nodes] return ';'.join(filter_specs) -def _get_global_args(node): +def _get_global_args(node: GlobalNode) -> list[str | int]: return list(node.args) -def _get_output_args(node, stream_name_map): +def _get_output_args(node: OutputNode, stream_name_map: StreamNameMap) -> list[str | int]: if node.name != output.__name__: raise ValueError('Unsupported output node: {}'.format(node)) - args = [] + args: list[str | int] = [] if len(node.incoming_edges) == 0: raise ValueError('Output node {} has no mapped streams'.format(node)) for edge in node.incoming_edges: # edge = node.incoming_edges[0] - stream_name = _format_input_stream_name( - stream_name_map, edge, is_final_arg=True - ) + stream_name = _format_input_stream_name(stream_name_map, edge, is_final_arg=True) if stream_name != '0' or len(node.incoming_edges) > 1: args += ['-map', stream_name] kwargs = copy.copy(node.kwargs) - filename = kwargs.pop('filename') + filename = str(kwargs.pop('filename')) if 'format' in kwargs: - args += ['-f', kwargs.pop('format')] + format = str(kwargs.pop('format')) + args += ['-f', format] if 'video_bitrate' in kwargs: - args += ['-b:v', str(kwargs.pop('video_bitrate'))] + video_bitrate = str(kwargs.pop('video_bitrate')) + args += ['-b:v', video_bitrate] if 'audio_bitrate' in kwargs: - args += ['-b:a', str(kwargs.pop('audio_bitrate'))] + audio_bitrate = str(kwargs.pop('audio_bitrate')) + args += ['-b:a', audio_bitrate] if 'video_size' in kwargs: video_size = kwargs.pop('video_size') - if not isinstance(video_size, basestring) and isinstance(video_size, Iterable): + if not isinstance(video_size, str) and isinstance(video_size, Iterable): video_size = '{}x{}'.format(video_size[0], video_size[1]) args += ['-video_size', video_size] args += convert_kwargs_to_cmd_line_args(kwargs) @@ -149,24 +142,22 @@ def _get_output_args(node, stream_name_map): @output_operator() -def get_args(stream_spec, overwrite_output=False): +def get_args(stream_spec: OutputStream, overwrite_output: bool = False) -> list[str | int]: """Build command-line arguments to be passed to ffmpeg.""" nodes = get_stream_spec_nodes(stream_spec) - args = [] + args: list[str | int] = [] # TODO: group nodes together, e.g. `-i somefile -r somerate`. sorted_nodes, outgoing_edge_maps = topo_sort(nodes) input_nodes = [node for node in sorted_nodes if isinstance(node, InputNode)] output_nodes = [node for node in sorted_nodes if isinstance(node, OutputNode)] global_nodes = [node for node in sorted_nodes if isinstance(node, GlobalNode)] filter_nodes = [node for node in sorted_nodes if isinstance(node, FilterNode)] - stream_name_map = {(node, None): str(i) for i, node in enumerate(input_nodes)} + stream_name_map: StreamNameMap = {(node, None): str(i) for i, node in enumerate(input_nodes)} filter_arg = _get_filter_arg(filter_nodes, outgoing_edge_maps, stream_name_map) args += reduce(operator.add, [_get_input_args(node) for node in input_nodes]) if filter_arg: args += ['-filter_complex', filter_arg] - args += reduce( - operator.add, [_get_output_args(node, stream_name_map) for node in output_nodes] - ) + args += reduce(operator.add, [_get_output_args(node, stream_name_map) for node in output_nodes]) args += reduce(operator.add, [_get_global_args(node) for node in global_nodes], []) if overwrite_output: args += ['-y'] @@ -174,7 +165,7 @@ def get_args(stream_spec, overwrite_output=False): @output_operator() -def compile(stream_spec, cmd='ffmpeg', overwrite_output=False): +def compile(stream_spec: OutputStream, cmd: list[str] | str = 'ffmpeg', overwrite_output: bool = False) -> list[str]: """Build command-line for invoking ffmpeg. The :meth:`run` function uses this to build the command line @@ -185,7 +176,7 @@ def compile(stream_spec, cmd='ffmpeg', overwrite_output=False): This is the same as calling :meth:`get_args` except that it also includes the ``ffmpeg`` command as the first argument. """ - if isinstance(cmd, basestring): + if isinstance(cmd, str): cmd = [cmd] elif type(cmd) != list: cmd = list(cmd) @@ -194,15 +185,15 @@ def compile(stream_spec, cmd='ffmpeg', overwrite_output=False): @output_operator() def run_async( - stream_spec, - cmd='ffmpeg', - pipe_stdin=False, - pipe_stdout=False, - pipe_stderr=False, - quiet=False, - overwrite_output=False, - cwd=None, -): + stream_spec: OutputStream, + cmd: str | list[str] = 'ffmpeg', + pipe_stdin: bool = False, + pipe_stdout: bool = False, + pipe_stderr: bool = False, + quiet: bool = False, + overwrite_output: bool = False, + cwd: str | None = None, +) -> subprocess.Popen[bytes]: """Asynchronously invoke ffmpeg for the supplied node graph. Args: @@ -298,15 +289,15 @@ def run_async( @output_operator() def run( - stream_spec, - cmd='ffmpeg', - capture_stdout=False, - capture_stderr=False, - input=None, - quiet=False, - overwrite_output=False, - cwd=None, -): + stream_spec: OutputStream, + cmd: str | list[str] = 'ffmpeg', + capture_stdout: bool = False, + capture_stderr: bool = False, + input: str | None = None, + quiet: bool = False, + overwrite_output: bool = False, + cwd: str | None = None, +) -> tuple[str, str]: """Invoke ffmpeg for the supplied node graph. Args: diff --git a/ffmpeg/_utils.py b/ffmpeg/_utils.py index 9baa2c78..c26cea72 100644 --- a/ffmpeg/_utils.py +++ b/ffmpeg/_utils.py @@ -1,97 +1,50 @@ from __future__ import unicode_literals -from builtins import str -from past.builtins import basestring -import hashlib -import sys - - -if sys.version_info.major == 2: - # noinspection PyUnresolvedReferences,PyShadowingBuiltins - str = str - -try: - from collections.abc import Iterable -except ImportError: - from collections import Iterable - - -# `past.builtins.basestring` module can't be imported on Python3 in some environments (Ubuntu). -# This code is copy-pasted from it to avoid crashes. -class BaseBaseString(type): - def __instancecheck__(cls, instance): - return isinstance(instance, (bytes, str)) - - def __subclasshook__(cls, thing): - # TODO: What should go here? - raise NotImplemented - -def with_metaclass(meta, *bases): - class metaclass(meta): - __call__ = type.__call__ - __init__ = type.__init__ - - def __new__(cls, name, this_bases, d): - if this_bases is None: - return type.__new__(cls, name, (), d) - return meta(name, bases, d) - - return metaclass('temporary_class', None, {}) - - -if sys.version_info.major >= 3: - - class basestring(with_metaclass(BaseBaseString)): - pass - -else: - # noinspection PyUnresolvedReferences,PyCompatibility - from builtins import basestring +import hashlib +from collections.abc import Iterable +from typing import Any, Mapping -def _recursive_repr(item): +def _recursive_repr(item: str | list[Any] | dict[str, Any]) -> str: """Hack around python `repr` to deterministically represent dictionaries. This is able to represent more things than json.dumps, since it does not require things to be JSON serializable (e.g. datetimes). """ - if isinstance(item, basestring): + if isinstance(item, str): result = str(item) elif isinstance(item, list): result = '[{}]'.format(', '.join([_recursive_repr(x) for x in item])) elif isinstance(item, dict): - kv_pairs = [ - '{}: {}'.format(_recursive_repr(k), _recursive_repr(item[k])) - for k in sorted(item) - ] + kv_pairs = ['{}: {}'.format(_recursive_repr(k), _recursive_repr(item[k])) for k in sorted(item)] result = '{' + ', '.join(kv_pairs) + '}' else: result = repr(item) return result -def get_hash(item): +def get_hash(item: str | list[Any] | dict[str, Any]) -> str: repr_ = _recursive_repr(item).encode('utf-8') return hashlib.md5(repr_).hexdigest() -def get_hash_int(item): +def get_hash_int(item: str | list[Any] | dict[str, Any]) -> int: return int(get_hash(item), base=16) -def escape_chars(text, chars): +def escape_chars(text: str | int | tuple[int, int], chars: str) -> str: """Helper function to escape uncomfortable characters.""" text = str(text) - chars = list(set(chars)) - if '\\' in chars: - chars.remove('\\') - chars.insert(0, '\\') - for ch in chars: + _chars = list(set(chars)) + if '\\' in _chars: + _chars.remove('\\') + _chars.insert(0, '\\') + for ch in _chars: text = text.replace(ch, '\\' + ch) return text -def convert_kwargs_to_cmd_line_args(kwargs): +def convert_kwargs_to_cmd_line_args(kwargs: Mapping[str, Any]) -> list[str]: """Helper function to build command line arguments out of dict.""" args = [] for k in sorted(kwargs.keys()): diff --git a/ffmpeg/_view.py b/ffmpeg/_view.py index 31955afd..5e321b20 100644 --- a/ffmpeg/_view.py +++ b/ffmpeg/_view.py @@ -1,23 +1,15 @@ from __future__ import unicode_literals -from builtins import str -from .dag import get_outgoing_edges -from ._run import topo_sort import tempfile -from ffmpeg.nodes import ( - FilterNode, - get_stream_spec_nodes, - InputNode, - OutputNode, - stream_operator, -) +from ffmpeg.nodes import FilterNode, InputNode, OutputNode, Stream, get_stream_spec_nodes, stream_operator +from .dag import KwargReprNode, get_outgoing_edges, topo_sort _RIGHT_ARROW = '\u2192' -def _get_node_color(node): +def _get_node_color(node: KwargReprNode) -> str | None: if isinstance(node, InputNode): color = '#99cc00' elif isinstance(node, OutputNode): @@ -30,13 +22,14 @@ def _get_node_color(node): @stream_operator() -def view(stream_spec, detail=False, filename=None, pipe=False, **kwargs): +def view( + stream_spec: Stream, detail: bool = False, filename: str | None = None, pipe: bool = False, **kwargs: str +) -> Stream | bytes: try: import graphviz except ImportError: raise ImportError( - 'failed to import graphviz; please make sure graphviz is installed (e.g. ' - '`pip install graphviz`)' + 'failed to import graphviz; please make sure graphviz is installed (e.g. ' '`pip install graphviz`)' ) show_labels = kwargs.pop('show_labels', True) @@ -51,9 +44,7 @@ def view(stream_spec, detail=False, filename=None, pipe=False, **kwargs): graph = graphviz.Digraph(format='png') graph.attr(rankdir='LR') if len(list(kwargs.keys())) != 0: - raise ValueError( - 'Invalid kwargs key(s): {}'.format(', '.join(list(kwargs.keys()))) - ) + raise ValueError('Invalid kwargs key(s): {}'.format(', '.join(list(kwargs.keys())))) for node in sorted_nodes: color = _get_node_color(node) @@ -61,15 +52,11 @@ def view(stream_spec, detail=False, filename=None, pipe=False, **kwargs): if detail: lines = [node.short_repr] lines += ['{!r}'.format(arg) for arg in node.args] - lines += [ - '{}={!r}'.format(key, node.kwargs[key]) for key in sorted(node.kwargs) - ] + lines += ['{}={!r}'.format(key, node.kwargs[key]) for key in sorted(node.kwargs)] node_text = '\n'.join(lines) else: node_text = node.short_repr - graph.node( - str(hash(node)), node_text, shape='box', style='filled', fillcolor=color - ) + graph.node(str(hash(node)), node_text, shape='box', style='filled', fillcolor=color) outgoing_edge_map = outgoing_edge_maps.get(node, {}) for edge in get_outgoing_edges(node, outgoing_edge_map): @@ -78,11 +65,7 @@ def view(stream_spec, detail=False, filename=None, pipe=False, **kwargs): down_label = edge.downstream_label up_selector = edge.upstream_selector - if show_labels and ( - up_label is not None - or down_label is not None - or up_selector is not None - ): + if show_labels and (up_label is not None or down_label is not None or up_selector is not None): if up_label is None: up_label = '' if up_selector is not None: diff --git a/ffmpeg/dag.py b/ffmpeg/dag.py index 4bdac44d..2e021ccb 100644 --- a/ffmpeg/dag.py +++ b/ffmpeg/dag.py @@ -1,8 +1,9 @@ -from __future__ import unicode_literals +from __future__ import annotations, unicode_literals + +from dataclasses import dataclass +from typing import Sequence from ._utils import get_hash, get_hash_int -from builtins import object -from collections import namedtuple class DagNode(object): @@ -50,27 +51,27 @@ class DagNode(object): constant. """ - def __hash__(self): + def __hash__(self) -> int: """Return an integer hash of the node.""" raise NotImplementedError() - def __eq__(self, other): + def __eq__(self, other: object) -> bool: """Compare two nodes; implementations should return True if (and only if) hashes match. """ raise NotImplementedError() - def __repr__(self, other): + def __repr__(self) -> str: """Return a full string representation of the node.""" raise NotImplementedError() @property - def short_repr(self): + def short_repr(self) -> str: """Return a partial/concise representation of the node.""" raise NotImplementedError() @property - def incoming_edge_map(self): + def incoming_edge_map(self) -> IncomingEdgeMap: """Provides information about all incoming edges that connect to this node. The edge map is a dictionary that maps an ``incoming_label`` to @@ -80,19 +81,19 @@ def incoming_edge_map(self): raise NotImplementedError() -DagEdge = namedtuple( - 'DagEdge', - [ - 'downstream_node', - 'downstream_label', - 'upstream_node', - 'upstream_label', - 'upstream_selector', - ], -) +@dataclass +class DagEdge: + downstream_node: KwargReprNode + downstream_label: int | None + upstream_node: KwargReprNode + upstream_label: int | None + upstream_selector: str | None -def get_incoming_edges(downstream_node, incoming_edge_map): +def get_incoming_edges( + downstream_node: KwargReprNode, + incoming_edge_map: IncomingEdgeMap, +) -> list[DagEdge]: edges = [] for downstream_label, upstream_info in list(incoming_edge_map.items()): upstream_node, upstream_label, upstream_selector = upstream_info @@ -108,7 +109,15 @@ def get_incoming_edges(downstream_node, incoming_edge_map): return edges -def get_outgoing_edges(upstream_node, outgoing_edge_map): +OutgoingEdgeMap = dict[None, list[tuple["KwargReprNode", str, None | str]]] + +IncomingEdgeMap = dict[int | None, tuple["KwargReprNode", str, str | None]] + + +def get_outgoing_edges( + upstream_node: KwargReprNode, + outgoing_edge_map: OutgoingEdgeMap, +) -> list[DagEdge]: edges = [] for upstream_label, downstream_infos in sorted(outgoing_edge_map.items()): for downstream_info in downstream_infos: @@ -129,7 +138,7 @@ class KwargReprNode(DagNode): """A DagNode that can be represented as a set of args+kwargs.""" @property - def __upstream_hashes(self): + def __upstream_hashes(self) -> list[int]: hashes = [] for downstream_label, upstream_info in list(self.incoming_edge_map.items()): upstream_node, upstream_label, upstream_selector = upstream_info @@ -145,78 +154,82 @@ def __upstream_hashes(self): return hashes @property - def __inner_hash(self): + def __inner_hash(self) -> str: props = {'args': self.args, 'kwargs': self.kwargs} return get_hash(props) - def __get_hash(self): + def __get_hash(self) -> int: hashes = self.__upstream_hashes + [self.__inner_hash] return get_hash_int(hashes) - def __init__(self, incoming_edge_map, name, args, kwargs): + def __init__( + self, + incoming_edge_map: IncomingEdgeMap, + name: str, + args: Sequence[str | int], + kwargs: dict[str, str | int | tuple[int, int]], + ): self.__incoming_edge_map = incoming_edge_map self.name = name self.args = args self.kwargs = kwargs self.__hash = self.__get_hash() - def __hash__(self): + def __hash__(self) -> int: return self.__hash - def __eq__(self, other): + def __eq__(self, other: object) -> bool: return hash(self) == hash(other) @property - def short_hash(self): + def short_hash(self) -> str: return '{:x}'.format(abs(hash(self)))[:12] - def long_repr(self, include_hash=True): + def long_repr(self, include_hash: bool = True) -> str: formatted_props = ['{!r}'.format(arg) for arg in self.args] - formatted_props += [ - '{}={!r}'.format(key, self.kwargs[key]) for key in sorted(self.kwargs) - ] + formatted_props += ['{}={!r}'.format(key, self.kwargs[key]) for key in sorted(self.kwargs)] out = '{}({})'.format(self.name, ', '.join(formatted_props)) if include_hash: out += ' <{}>'.format(self.short_hash) return out - def __repr__(self): + def __repr__(self) -> str: return self.long_repr() @property - def incoming_edges(self): + def incoming_edges(self) -> list[DagEdge]: return get_incoming_edges(self, self.incoming_edge_map) @property - def incoming_edge_map(self): + def incoming_edge_map(self) -> IncomingEdgeMap: return self.__incoming_edge_map @property - def short_repr(self): + def short_repr(self) -> str: return self.name -def topo_sort(downstream_nodes): +def topo_sort( + downstream_nodes: Sequence[KwargReprNode], +) -> tuple[list[KwargReprNode], dict[KwargReprNode, OutgoingEdgeMap]]: marked_nodes = [] - sorted_nodes = [] - outgoing_edge_maps = {} + sorted_nodes: list[KwargReprNode] = [] + outgoing_edge_maps: dict[KwargReprNode, OutgoingEdgeMap] = {} def visit( - upstream_node, - upstream_label, - downstream_node, - downstream_label, - downstream_selector=None, - ): + upstream_node: KwargReprNode, + upstream_label: None, + downstream_node: None, + downstream_label: None, + downstream_selector: None = None, + ) -> None: if upstream_node in marked_nodes: raise RuntimeError('Graph is not a DAG') if downstream_node is not None: - outgoing_edge_map = outgoing_edge_maps.get(upstream_node, {}) - outgoing_edge_infos = outgoing_edge_map.get(upstream_label, []) - outgoing_edge_infos += [ - (downstream_node, downstream_label, downstream_selector) - ] + outgoing_edge_map: OutgoingEdgeMap = outgoing_edge_maps.get(upstream_node, {}) + outgoing_edge_infos: list[tuple[KwargReprNode, str, None]] = outgoing_edge_map.get(upstream_label, []) + outgoing_edge_infos += [(downstream_node, downstream_label, downstream_selector)] outgoing_edge_map[upstream_label] = outgoing_edge_infos outgoing_edge_maps[upstream_node] = outgoing_edge_map diff --git a/ffmpeg/nodes.py b/ffmpeg/nodes.py index e8b28385..4047a47e 100644 --- a/ffmpeg/nodes.py +++ b/ffmpeg/nodes.py @@ -1,22 +1,14 @@ -from __future__ import unicode_literals +from __future__ import annotations, unicode_literals -from past.builtins import basestring -from .dag import KwargReprNode -from ._utils import escape_chars, get_hash_int -from builtins import object import os +from collections.abc import Iterable +from typing import Any, Callable, Mapping, Sequence, TypeVar - -def _is_of_types(obj, types): - valid = False - for stream_type in types: - if isinstance(obj, stream_type): - valid = True - break - return valid +from ._utils import escape_chars, get_hash_int +from .dag import DagEdge, IncomingEdgeMap, KwargReprNode -def _get_types_str(types): +def _get_types_str(types: Iterable[Any]) -> str: return ', '.join(['{}.{}'.format(x.__module__, x.__name__) for x in types]) @@ -26,9 +18,13 @@ class Stream(object): """ def __init__( - self, upstream_node, upstream_label, node_types, upstream_selector=None + self, + upstream_node: Node, + upstream_label: str, + node_types: Iterable[type], + upstream_selector: str | None = None, ): - if not _is_of_types(upstream_node, node_types): + if not isinstance(upstream_node, tuple(node_types)): raise TypeError( 'Expected upstream node to be of one of the following type(s): {}; got {}'.format( _get_types_str(node_types), type(upstream_node) @@ -38,23 +34,21 @@ def __init__( self.label = upstream_label self.selector = upstream_selector - def __hash__(self): + def __hash__(self) -> int: return get_hash_int([hash(self.node), hash(self.label)]) - def __eq__(self, other): + def __eq__(self, other: object) -> bool: return hash(self) == hash(other) - def __repr__(self): + def __repr__(self) -> str: node_repr = self.node.long_repr(include_hash=False) selector = '' if self.selector: selector = ':{}'.format(self.selector) - out = '{}[{!r}{}] <{}>'.format( - node_repr, self.label, selector, self.node.short_hash - ) + out = '{}[{!r}{}] <{}>'.format(node_repr, self.label, selector, self.node.short_hash) return out - def __getitem__(self, index): + def __getitem__(self, index: str) -> Stream: """ Select a component (audio, video) of the stream. @@ -68,12 +62,12 @@ def __getitem__(self, index): """ if self.selector is not None: raise ValueError('Stream already has a selector: {}'.format(self)) - elif not isinstance(index, basestring): + elif not isinstance(index, str): raise TypeError("Expected string index (e.g. 'a'); got {!r}".format(index)) return self.node.stream(label=self.label, selector=index) @property - def audio(self): + def audio(self) -> Stream: """Select the audio-portion of a stream. Some ffmpeg filters drop audio streams, and care must be taken @@ -98,7 +92,7 @@ def audio(self): return self['a'] @property - def video(self): + def video(self) -> Stream: """Select the video-portion of a stream. Some ffmpeg filters drop audio streams, and care must be taken @@ -123,19 +117,24 @@ def video(self): return self['v'] -def get_stream_map(stream_spec): +StreamSpec = Mapping[int | None, Stream] | Stream | Sequence[Stream] | None +StreamMap = dict[int | None, Stream] + + +def get_stream_map(stream_spec: StreamSpec) -> StreamMap: + stream_map: StreamMap if stream_spec is None: stream_map = {} elif isinstance(stream_spec, Stream): stream_map = {None: stream_spec} elif isinstance(stream_spec, (list, tuple)): - stream_map = dict(enumerate(stream_spec)) + stream_map = {i: item for i, item in enumerate(stream_spec)} elif isinstance(stream_spec, dict): stream_map = stream_spec return stream_map -def get_stream_map_nodes(stream_map): +def get_stream_map_nodes(stream_map: StreamMap) -> list[Node]: nodes = [] for stream in list(stream_map.values()): if not isinstance(stream, Stream): @@ -144,7 +143,7 @@ def get_stream_map_nodes(stream_map): return nodes -def get_stream_spec_nodes(stream_spec): +def get_stream_spec_nodes(stream_spec: StreamSpec) -> list[Node]: stream_map = get_stream_map(stream_spec) return get_stream_map_nodes(stream_map) @@ -153,24 +152,16 @@ class Node(KwargReprNode): """Node base""" @classmethod - def __check_input_len(cls, stream_map, min_inputs, max_inputs): + def __check_input_len(cls, stream_map: StreamMap, min_inputs: int | None, max_inputs: int | None) -> None: if min_inputs is not None and len(stream_map) < min_inputs: - raise ValueError( - 'Expected at least {} input stream(s); got {}'.format( - min_inputs, len(stream_map) - ) - ) + raise ValueError('Expected at least {} input stream(s); got {}'.format(min_inputs, len(stream_map))) elif max_inputs is not None and len(stream_map) > max_inputs: - raise ValueError( - 'Expected at most {} input stream(s); got {}'.format( - max_inputs, len(stream_map) - ) - ) + raise ValueError('Expected at most {} input stream(s); got {}'.format(max_inputs, len(stream_map))) @classmethod - def __check_input_types(cls, stream_map, incoming_stream_types): + def __check_input_types(cls, stream_map: StreamMap, incoming_stream_types: tuple[type, ...] | set[type]) -> None: for stream in list(stream_map.values()): - if not _is_of_types(stream, incoming_stream_types): + if not isinstance(stream, tuple(incoming_stream_types)): raise TypeError( 'Expected incoming stream(s) to be of one of the following types: {}; got {}'.format( _get_types_str(incoming_stream_types), type(stream) @@ -178,8 +169,8 @@ def __check_input_types(cls, stream_map, incoming_stream_types): ) @classmethod - def __get_incoming_edge_map(cls, stream_map): - incoming_edge_map = {} + def __get_incoming_edge_map(cls, stream_map: StreamMap) -> IncomingEdgeMap: + incoming_edge_map: IncomingEdgeMap = {} for downstream_label, upstream in list(stream_map.items()): incoming_edge_map[downstream_label] = ( upstream.node, @@ -190,14 +181,14 @@ def __get_incoming_edge_map(cls, stream_map): def __init__( self, - stream_spec, - name, - incoming_stream_types, - outgoing_stream_type, - min_inputs, - max_inputs, - args=[], - kwargs={}, + stream_spec: StreamSpec, + name: str, + incoming_stream_types: tuple[type, ...] | set[type], + outgoing_stream_type: type, + min_inputs: int, + max_inputs: int | None, + args: Sequence[str | int] = [], + kwargs: dict[str, str | int | tuple[int, int]] = {}, ): stream_map = get_stream_map(stream_spec) self.__check_input_len(stream_map, min_inputs, max_inputs) @@ -208,14 +199,14 @@ def __init__( self.__outgoing_stream_type = outgoing_stream_type self.__incoming_stream_types = incoming_stream_types - def stream(self, label=None, selector=None): + def stream(self, label: str | None = None, selector: str | None = None) -> Stream: """Create an outgoing stream originating from this node. More nodes may be attached onto the outgoing stream. """ return self.__outgoing_stream_type(self, label, upstream_selector=selector) - def __getitem__(self, item): + def __getitem__(self, item: str | slice) -> Stream: """Create an outgoing stream originating from this node; syntactic sugar for ``self.stream(label)``. It can also be used to apply a selector: e.g. ``node[0:'a']`` returns a stream with label 0 and selector ``'a'``, which is @@ -236,7 +227,7 @@ def __getitem__(self, item): class FilterableStream(Stream): - def __init__(self, upstream_node, upstream_label, upstream_selector=None): + def __init__(self, upstream_node: Node, upstream_label: str, upstream_selector: None = None): super(FilterableStream, self).__init__( upstream_node, upstream_label, {InputNode, FilterNode}, upstream_selector ) @@ -246,11 +237,11 @@ def __init__(self, upstream_node, upstream_label, upstream_selector=None): class InputNode(Node): """InputNode type""" - def __init__(self, name, args=[], kwargs={}): + def __init__(self, name: str, args: list[str] = [], kwargs: dict[str, str | int | tuple[int, int]] = {}): super(InputNode, self).__init__( stream_spec=None, name=name, - incoming_stream_types={}, + incoming_stream_types=set(), outgoing_stream_type=FilterableStream, min_inputs=0, max_inputs=0, @@ -259,13 +250,20 @@ def __init__(self, name, args=[], kwargs={}): ) @property - def short_repr(self): - return os.path.basename(self.kwargs['filename']) + def short_repr(self) -> str: + return os.path.basename(str(self.kwargs['filename'])) # noinspection PyMethodOverriding class FilterNode(Node): - def __init__(self, stream_spec, name, max_inputs=1, args=[], kwargs={}): + def __init__( + self, + stream_spec: Stream, + name: str, + max_inputs: int = 1, + args: list[str] = [], + kwargs: dict[str, str | int | tuple[int, int]] = {}, + ): super(FilterNode, self).__init__( stream_spec=stream_spec, name=name, @@ -279,7 +277,7 @@ def __init__(self, stream_spec, name, max_inputs=1, args=[], kwargs={}): """FilterNode""" - def _get_filter(self, outgoing_edges): + def _get_filter(self, outgoing_edges: list[DagEdge]) -> str: args = self.args kwargs = self.kwargs if self.name in ('split', 'asplit'): @@ -305,7 +303,9 @@ def _get_filter(self, outgoing_edges): # noinspection PyMethodOverriding class OutputNode(Node): - def __init__(self, stream, name, args=[], kwargs={}): + def __init__( + self, stream: Stream | tuple[Stream, ...], name: str, args: list[Any] = [], kwargs: dict[str, Any] = {} + ): super(OutputNode, self).__init__( stream_spec=stream, name=name, @@ -318,12 +318,12 @@ def __init__(self, stream, name, args=[], kwargs={}): ) @property - def short_repr(self): - return os.path.basename(self.kwargs['filename']) + def short_repr(self) -> str: + return os.path.basename(str(self.kwargs['filename'])) class OutputStream(Stream): - def __init__(self, upstream_node, upstream_label, upstream_selector=None): + def __init__(self, upstream_node: Node, upstream_label: str, upstream_selector: str | None = None): super(OutputStream, self).__init__( upstream_node, upstream_label, @@ -334,7 +334,7 @@ def __init__(self, upstream_node, upstream_label, upstream_selector=None): # noinspection PyMethodOverriding class MergeOutputsNode(Node): - def __init__(self, streams, name): + def __init__(self, streams: tuple[Stream, ...], name: str): super(MergeOutputsNode, self).__init__( stream_spec=streams, name=name, @@ -347,7 +347,9 @@ def __init__(self, streams, name): # noinspection PyMethodOverriding class GlobalNode(Node): - def __init__(self, stream, name, args=[], kwargs={}): + def __init__( + self, stream: Stream, name: str, args: Sequence[str] = (), kwargs: dict[str, str | int | tuple[int, int]] = {} + ): super(GlobalNode, self).__init__( stream_spec=stream, name=name, @@ -360,20 +362,26 @@ def __init__(self, stream, name, args=[], kwargs={}): ) -def stream_operator(stream_classes={Stream}, name=None): - def decorator(func): +RT = TypeVar('RT') + + +def stream_operator(stream_classes: set[type] = {Stream}, name: str | None = None) -> Callable[..., Any]: + def decorator(func: Callable[..., Any]) -> Callable[..., Any]: func_name = name or func.__name__ - [setattr(stream_class, func_name, func) for stream_class in stream_classes] + + for stream_class in stream_classes: + setattr(stream_class, func_name, func) + return func return decorator -def filter_operator(name=None): +def filter_operator(name: str | None = None) -> Callable[..., Any]: return stream_operator(stream_classes={FilterableStream}, name=name) -def output_operator(name=None): +def output_operator(name: str | None = None) -> Callable[..., Any]: return stream_operator(stream_classes={OutputStream}, name=name) diff --git a/ffmpeg/tests/test_ffmpeg.py b/ffmpeg/tests/test_ffmpeg.py index 8dbc271a..5904fb14 100644 --- a/ffmpeg/tests/test_ffmpeg.py +++ b/ffmpeg/tests/test_ffmpeg.py @@ -1,15 +1,15 @@ from __future__ import unicode_literals -from builtins import bytes -from builtins import range -from builtins import str -import ffmpeg + import os -import pytest import random import re import subprocess import sys +from builtins import bytes, range, str +import pytest + +import ffmpeg try: import mock # python 2 @@ -32,10 +32,7 @@ def test_escape_chars(): assert ffmpeg._utils.escape_chars('a:b', ':') == r'a\:b' assert ffmpeg._utils.escape_chars('a\\:b', ':\\') == 'a\\\\\\:b' - assert ( - ffmpeg._utils.escape_chars('a:b,c[d]e%{}f\'g\'h\\i', '\\\':,[]%') - == 'a\\:b\\,c\\[d\\]e\\%{}f\\\'g\\\'h\\\\i' - ) + assert ffmpeg._utils.escape_chars('a:b,c[d]e%{}f\'g\'h\\i', '\\\':,[]%') == 'a\\:b\\,c\\[d\\]e\\%{}f\\\'g\\\'h\\\\i' assert ffmpeg._utils.escape_chars(123, ':\\') == '123' @@ -86,41 +83,25 @@ def test_node_repr(): trim3 = ffmpeg.trim(in_file, start_frame=50, end_frame=60) concatted = ffmpeg.concat(trim1, trim2, trim3) output = ffmpeg.output(concatted, 'dummy2.mp4') - assert repr(in_file.node) == 'input(filename={!r}) <{}>'.format( - 'dummy.mp4', in_file.node.short_hash - ) - assert repr(trim1.node) == 'trim(end_frame=20, start_frame=10) <{}>'.format( - trim1.node.short_hash - ) - assert repr(trim2.node) == 'trim(end_frame=40, start_frame=30) <{}>'.format( - trim2.node.short_hash - ) - assert repr(trim3.node) == 'trim(end_frame=60, start_frame=50) <{}>'.format( - trim3.node.short_hash - ) + assert repr(in_file.node) == 'input(filename={!r}) <{}>'.format('dummy.mp4', in_file.node.short_hash) + assert repr(trim1.node) == 'trim(end_frame=20, start_frame=10) <{}>'.format(trim1.node.short_hash) + assert repr(trim2.node) == 'trim(end_frame=40, start_frame=30) <{}>'.format(trim2.node.short_hash) + assert repr(trim3.node) == 'trim(end_frame=60, start_frame=50) <{}>'.format(trim3.node.short_hash) assert repr(concatted.node) == 'concat(n=3) <{}>'.format(concatted.node.short_hash) - assert repr(output.node) == 'output(filename={!r}) <{}>'.format( - 'dummy2.mp4', output.node.short_hash - ) + assert repr(output.node) == 'output(filename={!r}) <{}>'.format('dummy2.mp4', output.node.short_hash) def test_stream_repr(): in_file = ffmpeg.input('dummy.mp4') - assert repr(in_file) == 'input(filename={!r})[None] <{}>'.format( - 'dummy.mp4', in_file.node.short_hash - ) + assert repr(in_file) == 'input(filename={!r})[None] <{}>'.format('dummy.mp4', in_file.node.short_hash) split0 = in_file.filter_multi_output('split')[0] assert repr(split0) == 'split()[0] <{}>'.format(split0.node.short_hash) dummy_out = in_file.filter_multi_output('dummy')['out'] - assert repr(dummy_out) == 'dummy()[{!r}] <{}>'.format( - dummy_out.label, dummy_out.node.short_hash - ) + assert repr(dummy_out) == 'dummy()[{!r}] <{}>'.format(dummy_out.label, dummy_out.node.short_hash) def test_repeated_args(): - out_file = ffmpeg.input('dummy.mp4').output( - 'dummy2.mp4', streamid=['0:0x101', '1:0x102'] - ) + out_file = ffmpeg.input('dummy.mp4').output('dummy2.mp4', streamid=['0:0x101', '1:0x102']) assert out_file.get_args() == [ '-i', 'dummy.mp4', @@ -138,11 +119,7 @@ def test__get_args__simple(): def test_global_args(): - out_file = ( - ffmpeg.input('dummy.mp4') - .output('dummy2.mp4') - .global_args('-progress', 'someurl') - ) + out_file = ffmpeg.input('dummy.mp4').output('dummy2.mp4').global_args('-progress', 'someurl') assert out_file.get_args() == [ '-i', 'dummy.mp4', @@ -330,10 +307,7 @@ def test_filter_concat__wrong_stream_count(): in2 = ffmpeg.input('in2.mp4') with pytest.raises(ValueError) as excinfo: ffmpeg.concat(in1.video, in1.audio, in2.hflip(), v=1, a=1).node - assert ( - str(excinfo.value) - == 'Expected concat input streams to have length multiple of 2 (v=1, a=1); got 3' - ) + assert str(excinfo.value) == 'Expected concat input streams to have length multiple of 2 (v=1, a=1); got 3' def test_filter_asplit(): @@ -358,11 +332,7 @@ def test_filter_asplit(): def test__output__bitrate(): - args = ( - ffmpeg.input('in') - .output('out', video_bitrate=1000, audio_bitrate=200) - .get_args() - ) + args = ffmpeg.input('in').output('out', video_bitrate=1000, audio_bitrate=200).get_args() assert args == ['-i', 'in', '-b:v', '1000', '-b:a', '200', 'out'] @@ -381,12 +351,7 @@ def _get_drawtext_font_repr(font): """Build a command-line arg using drawtext ``font`` param and extract the ``-filter_complex`` arg. """ - args = ( - ffmpeg.input('in') - .drawtext('test', font='a{}b'.format(font)) - .output('out') - .get_args() - ) + args = ffmpeg.input('in').drawtext('test', font='a{}b'.format(font)).output('out').get_args() assert args[:3] == ['-i', 'in', '-filter_complex'] assert args[4:] == ['-map', '[s0]', 'out'] match = re.match( @@ -506,9 +471,7 @@ def test__run(): def test__run__capture_out(mocker, capture_stdout, capture_stderr): mocker.patch.object(ffmpeg._run, 'compile', return_value=['echo', 'test']) stream = _get_simple_example() - out, err = ffmpeg.run( - stream, capture_stdout=capture_stdout, capture_stderr=capture_stderr - ) + out, err = ffmpeg.run(stream, capture_stdout=capture_stdout, capture_stderr=capture_stderr) if capture_stdout: assert out == 'test\n'.encode() else: @@ -533,9 +496,7 @@ def test__run__error(mocker, capture_stdout, capture_stderr): mocker.patch.object(ffmpeg._run, 'compile', return_value=['ffmpeg']) stream = _get_complex_filter_example() with pytest.raises(ffmpeg.Error) as excinfo: - out, err = ffmpeg.run( - stream, capture_stdout=capture_stdout, capture_stderr=capture_stderr - ) + out, err = ffmpeg.run(stream, capture_stdout=capture_stdout, capture_stderr=capture_stderr) assert str(excinfo.value) == 'ffmpeg error (see stderr output for detail)' out = excinfo.value.stdout err = excinfo.value.stderr @@ -582,11 +543,7 @@ def test__filter__custom(): def test__filter__custom_fluent(): - stream = ( - ffmpeg.input('dummy.mp4') - .filter('custom_filter', 'a', 'b', kwarg1='c') - .output('dummy2.mp4') - ) + stream = ffmpeg.input('dummy.mp4').filter('custom_filter', 'a', 'b', kwarg1='c').output('dummy2.mp4') assert stream.get_args() == [ '-i', 'dummy.mp4', @@ -732,9 +689,7 @@ def test_pipe(): stderr=subprocess.PIPE, ) - in_data = bytes( - bytearray([random.randint(0, 255) for _ in range(frame_size * frame_count)]) - ) + in_data = bytes(bytearray([random.randint(0, 255) for _ in range(frame_size * frame_count)])) p.stdin.write(in_data) # note: this could block, in which case need to use threads p.stdin.close() @@ -806,9 +761,7 @@ def test__get_filter_complex_outputs(): def test__multi_output_edge_label_order(): - scale2ref = ffmpeg.filter_multi_output( - [ffmpeg.input('x'), ffmpeg.input('y')], 'scale2ref' - ) + scale2ref = ffmpeg.filter_multi_output([ffmpeg.input('x'), ffmpeg.input('y')], 'scale2ref') out = ffmpeg.merge_outputs( scale2ref[1].filter('scale').output('a'), scale2ref[10000].filter('hflip').output('b'), diff --git a/pyproject.toml b/pyproject.toml index de71e58d..36c8ce4d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,5 @@ [tool.black] skip-string-normalization = true -target_version = ['py27'] # TODO: drop Python 2 support (... "Soon"). include = '\.pyi?$' exclude = ''' ( diff --git a/setup.py b/setup.py index 72f381cb..d25e76fb 100644 --- a/setup.py +++ b/setup.py @@ -1,10 +1,9 @@ -from setuptools import setup from textwrap import dedent +from setuptools import setup + version = '0.2.0' -download_url = 'https://github.com/kkroening/ffmpeg-python/archive/v{}.zip'.format( - version -) +download_url = 'https://github.com/kkroening/ffmpeg-python/archive/v{}.zip'.format(version) long_description = dedent( '''\ diff --git a/tox.ini b/tox.ini index 98814078..b083aed8 100644 --- a/tox.ini +++ b/tox.ini @@ -4,11 +4,10 @@ # and then run "tox" from this directory. [tox] -envlist = py27, py35, py36, py37, py38, py39, py310 +envlist = py35, py36, py37, py38, py39, py310 [gh-actions] python = - 2.7: py27 3.5: py35 3.6: py36 3.7: py37