├── DocumentationImages ├── 01 - UI-Initial-Interface.png ├── 02 - UI-Clip Addition.png ├── 03 - Multiple clips and markers.png ├── 04 - Filtering.png ├── 04b - Timeline Filtering.png ├── 05 - Sequencing and Transitions.png ├── 06 - Encoding.png ├── 07 - Menu Options.png ├── Example_KeyframedCropValues.gif ├── GridOutput.gif ├── Tracking-Example.gif ├── UI_preview.gif ├── exampleOutput.gif ├── exampleOverlayOuput_StainsHD.gif └── example_VR_head_tracking.gif ├── LICENSE ├── README.md ├── customEncodeprofiles ├── 4Chan 3Meg Webm with no Sound.json ├── 4Chan 4Meg Webm with sound vp9.json ├── 4Chan 4Meg Webm with sound.json ├── 4Chan 6Meg Webm with sound.json ├── Discord 100M limit mp4 with hwaccel.json ├── Discord 8M limit mp4 with hwaccel.json └── Discord 8M limit mp4.json ├── customEnoderSpecs ├── H265 mp4.json └── VP9 webm 10bpp.json ├── filterTemplates └── 4 Second Header Text.json ├── postFilters ├── PostFilter-addQRCode.txt ├── PostFilter-chromaShift.txt ├── PostFilter-mirror.txt ├── PostFilter-neonedge.txt ├── PostFilter-rainbow.txt └── PostFilter-vhsishclean.txt ├── requirements-linux.txt ├── requirements.txt ├── resources ├── QRCode.gif ├── RankImages │ ├── Rank-0-cell.png │ ├── Rank-A.png │ ├── Rank-B.png │ ├── Rank-C.png │ ├── Rank-D.png │ ├── Rank-E.png │ ├── Rank-F.png │ └── Rank-S.png ├── SundayMorning.otf ├── cascade │ ├── facefinder │ ├── lps │ │ ├── lp312 │ │ ├── lp38 │ │ ├── lp42 │ │ ├── lp44 │ │ ├── lp46 │ │ ├── lp81 │ │ ├── lp82 │ │ ├── lp84 │ │ └── lp93 │ └── puploc ├── cutPreview.png ├── helptextTemplate.html ├── icon.ico ├── icon.png ├── icons │ ├── alphabet-s.png │ ├── chevrons-right-arrows.png │ ├── clock-time.png │ ├── copy-duplicate.png │ ├── folder-add-file.png │ ├── folder-file-project.png │ ├── maximize-expand.png │ ├── media-film-video.png │ ├── more-horizontal.png │ ├── photo-image-picture.png │ ├── redo.png │ ├── save-alt.png │ ├── save.png │ ├── video-camera-media.png │ └── youtube-video.png ├── loadingPreview.png ├── playerbg.png ├── quicksand.otf ├── slider_left_base.gif ├── slider_left_light.gif ├── slider_right_base.gif ├── slider_right_light.gif ├── sliders.png ├── speechModel │ ├── model.rnnn │ └── weights.hdf5.xz └── voiceModel │ ├── model.rnnn │ └── weights.hdf5.xz ├── setup.py ├── src ├── __init__.py ├── composeController.py ├── composeUi.py ├── cutselectionController.py ├── cutselectionUi.py ├── encoders │ ├── __init__.py │ ├── apngEncoder.py │ ├── gifEncoder.py │ ├── gifskiEncoder.py │ ├── mp4AV1Encoder.py │ ├── mp4H265NvencEncoder.py │ ├── mp4x264Encoder.py │ ├── mp4x264NvencEncoder.py │ ├── specVideoEncoder.py │ ├── webmvp8Encoder.py │ └── webmvp9Encoder.py ├── encodingUtils.py ├── faceDetectionService.py ├── ffmpegInfoParser.py ├── ffmpegService.py ├── filterSelectionController.py ├── filterSelectionUi.py ├── filterSpec.py ├── filterSpecs.json ├── filterValuePair.py ├── masonry.py ├── mergeSelectionController.py ├── mergeSelectionUi.py ├── modalWindows.py ├── optimisers │ ├── __init__.py │ ├── linear.py │ └── nelderMead.py ├── screenspacetools.lua ├── subtitleCutter.py ├── timeLineSelectionFrameUI.py ├── videoClipSelectionFrameUI.py ├── videoManager.py ├── videoSequenceComposeFrameUI.py ├── voiceActivityService.py ├── vrscript.lua ├── webmGeneratorController.py ├── webmGeneratorUi.py └── youtubeDLService.py └── webmGenerator.py /DocumentationImages/01 - UI-Initial-Interface.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfaker/WebmGenerator/e6d06e086c15367c8d3b89f27f4c9c4e245b9db4/DocumentationImages/01 - UI-Initial-Interface.png -------------------------------------------------------------------------------- /DocumentationImages/02 - UI-Clip Addition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfaker/WebmGenerator/e6d06e086c15367c8d3b89f27f4c9c4e245b9db4/DocumentationImages/02 - UI-Clip Addition.png -------------------------------------------------------------------------------- /DocumentationImages/03 - Multiple clips and markers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfaker/WebmGenerator/e6d06e086c15367c8d3b89f27f4c9c4e245b9db4/DocumentationImages/03 - Multiple clips and markers.png -------------------------------------------------------------------------------- /DocumentationImages/04 - Filtering.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfaker/WebmGenerator/e6d06e086c15367c8d3b89f27f4c9c4e245b9db4/DocumentationImages/04 - Filtering.png -------------------------------------------------------------------------------- /DocumentationImages/04b - Timeline Filtering.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfaker/WebmGenerator/e6d06e086c15367c8d3b89f27f4c9c4e245b9db4/DocumentationImages/04b - Timeline Filtering.png -------------------------------------------------------------------------------- /DocumentationImages/05 - Sequencing and Transitions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfaker/WebmGenerator/e6d06e086c15367c8d3b89f27f4c9c4e245b9db4/DocumentationImages/05 - Sequencing and Transitions.png -------------------------------------------------------------------------------- /DocumentationImages/06 - Encoding.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfaker/WebmGenerator/e6d06e086c15367c8d3b89f27f4c9c4e245b9db4/DocumentationImages/06 - Encoding.png -------------------------------------------------------------------------------- /DocumentationImages/07 - Menu Options.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfaker/WebmGenerator/e6d06e086c15367c8d3b89f27f4c9c4e245b9db4/DocumentationImages/07 - Menu Options.png -------------------------------------------------------------------------------- /DocumentationImages/Example_KeyframedCropValues.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfaker/WebmGenerator/e6d06e086c15367c8d3b89f27f4c9c4e245b9db4/DocumentationImages/Example_KeyframedCropValues.gif -------------------------------------------------------------------------------- /DocumentationImages/GridOutput.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfaker/WebmGenerator/e6d06e086c15367c8d3b89f27f4c9c4e245b9db4/DocumentationImages/GridOutput.gif -------------------------------------------------------------------------------- /DocumentationImages/Tracking-Example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfaker/WebmGenerator/e6d06e086c15367c8d3b89f27f4c9c4e245b9db4/DocumentationImages/Tracking-Example.gif -------------------------------------------------------------------------------- /DocumentationImages/UI_preview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfaker/WebmGenerator/e6d06e086c15367c8d3b89f27f4c9c4e245b9db4/DocumentationImages/UI_preview.gif -------------------------------------------------------------------------------- /DocumentationImages/exampleOutput.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfaker/WebmGenerator/e6d06e086c15367c8d3b89f27f4c9c4e245b9db4/DocumentationImages/exampleOutput.gif -------------------------------------------------------------------------------- /DocumentationImages/exampleOverlayOuput_StainsHD.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfaker/WebmGenerator/e6d06e086c15367c8d3b89f27f4c9c4e245b9db4/DocumentationImages/exampleOverlayOuput_StainsHD.gif -------------------------------------------------------------------------------- /DocumentationImages/example_VR_head_tracking.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfaker/WebmGenerator/e6d06e086c15367c8d3b89f27f4c9c4e245b9db4/DocumentationImages/example_VR_head_tracking.gif -------------------------------------------------------------------------------- /customEncodeprofiles/4Chan 3Meg Webm with no Sound.json: -------------------------------------------------------------------------------- 1 | {"name":"4chan /tv/ - 3Meg webm with no sound","editable":false,"outputFormat":"webm:VP8","maximumSize":"3.0","audioChannels":"No audio"} -------------------------------------------------------------------------------- /customEncodeprofiles/4Chan 4Meg Webm with sound vp9.json: -------------------------------------------------------------------------------- 1 | {"name":"4chan /gif/ - VP9 - 4Meg webm with sound","editable":false,"outputFormat":"webm:VP9","maximumSize":"4.0","audioChannels":"Mono","audioRate":"64"} -------------------------------------------------------------------------------- /customEncodeprofiles/4Chan 4Meg Webm with sound.json: -------------------------------------------------------------------------------- 1 | {"name":"4chan /gif/ - 4Meg webm with sound","editable":false,"outputFormat":"webm:VP8","maximumSize":"4.0","audioChannels":"Mono","audioRate":"64"} -------------------------------------------------------------------------------- /customEncodeprofiles/4Chan 6Meg Webm with sound.json: -------------------------------------------------------------------------------- 1 | {"name":"4chan /wsg/ - 6Meg webm with sound","editable":false,"outputFormat":"webm:VP8","maximumSize":"6.0","audioChannels":"Mono","audioRate":"64"} -------------------------------------------------------------------------------- /customEncodeprofiles/Discord 100M limit mp4 with hwaccel.json: -------------------------------------------------------------------------------- 1 | {"name":"Discord basic 100M mp4 with hwaccel","editable":false,"outputFormat":"mp4:x264_Nvenc","maximumSize":"100.0","audioChannels":"Mono","audioRate":"64"} -------------------------------------------------------------------------------- /customEncodeprofiles/Discord 8M limit mp4 with hwaccel.json: -------------------------------------------------------------------------------- 1 | {"name":"Discord basic 8M mp4 with hwaccel","editable":false,"outputFormat":"mp4:x264_Nvenc","maximumSize":"8.0","audioChannels":"Mono","audioRate":"64"} -------------------------------------------------------------------------------- /customEncodeprofiles/Discord 8M limit mp4.json: -------------------------------------------------------------------------------- 1 | {"name":"Discord basic 8M mp4","editable":false,"outputFormat":"mp4:x264","maximumSize":"8.0","audioChannels":"Mono","audioRate":"64"} -------------------------------------------------------------------------------- /customEnoderSpecs/H265 mp4.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"H265 mp4", 3 | "baseEncoder":"", 4 | "extension":"mp4", 5 | "multi-pass-encode":true, 6 | "commandBlocks":[ 7 | {"cmds":["-shortest", "-copyts", "-start_at_zero", "-c:v","libx265"]}, 8 | 9 | {"conditions":[["audioChannels","contains","Copy"]], 10 | "cmds":["-c:a","libopus"], 11 | "altCmds":[] 12 | }, 13 | 14 | {"cmds":["-stats","-threads","{encoderStageThreads}"]}, 15 | 16 | {"conditions":[["audioChannels","contains","No audio"]], 17 | "cmds":["-an"] 18 | }, 19 | {"conditions":[["audioChannels","contains","Stereo"]], 20 | "cmds":["-ac","2","-ar","48k", "-b:a","{audoBitrate}"] 21 | }, 22 | {"conditions":[["audioChannels","contains","Mono"]], 23 | "cmds":["-ac","1","-ar","48k", "-b:a","{audoBitrate}"] 24 | }, 25 | {"conditions":[["audioChannels","contains","Copy"]], 26 | "cmds":["-c:a","copy"] 27 | }, 28 | {"cmds":["-sn"]} 29 | ] 30 | } -------------------------------------------------------------------------------- /customEnoderSpecs/VP9 webm 10bpp.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"VP9 10bpp", 3 | "baseEncoder":"", 4 | "extension":"webm", 5 | "multi-pass-encode":true, 6 | "commandBlocks":[ 7 | {"cmds":["-shortest", "-copyts", "-start_at_zero", "-c:v","libvpx-vp9"]}, 8 | 9 | 10 | {"conditions":[["audioChannels","contains","Copy"]], 11 | "cmds":["-c:a","libopus"], 12 | "altCmds":[] 13 | }, 14 | 15 | {"cmds":["-stats","-threads","{encoderStageThreads}"]}, 16 | 17 | 18 | {"selection":{ 19 | "name":"auto-alt-ref", 20 | "label":"VP9 auto-alt-ref", 21 | "default":"6", 22 | "type":"int", 23 | "cmds":["-auto-alt-ref", "{value}"] 24 | }}, 25 | 26 | {"selection":{ 27 | "name":"lag-in-frames", 28 | "label":"VP9 lag-in-frames", 29 | "default":"25", 30 | "type":"int", 31 | "cmds":["-lag-in-frames", "{value}"] 32 | }}, 33 | 34 | {"conditions":[["passPhase","equals","1"]], 35 | "cmds":[], 36 | "altCmds":["-speed", "1"] 37 | }, 38 | 39 | {"cmds":["-psnr", "-row-mt", "1", "-tile-columns", "{tileColumns}", "-tile-rows", "0" 40 | ,"-aq-mode", "0"] 41 | }, 42 | 43 | 44 | {"selection":{ 45 | "name":"arnr-maxframes", 46 | "label":"VP9 ARNR maximum frames", 47 | "default":"15", 48 | "type":"int", 49 | "cmds":["-arnr-maxframes", "{value}"] 50 | }}, 51 | 52 | {"selection":{ 53 | "name":"arnr-strength", 54 | "label":"VP9 ARNR noise filter strength", 55 | "default":"0", 56 | "type":"int", 57 | "cmds":["-arnr-strength", "{value}"] 58 | }}, 59 | 60 | {"selection":{ 61 | "name":"pixfmt", 62 | "label":"Output Pixel Format", 63 | "default":"yuv420p10le profile:2", 64 | "options":[ 65 | {"name":"yuv420p10le profile:2","cmds":["-profile:v", "2", "-pix_fmt", "yuv420p10le"]}, 66 | {"name":"yuv420p profile:0","cmds":["-profile:v", "0", "-pix_fmt", "yuv420p"]} 67 | ] 68 | }}, 69 | 70 | {"selection":{ 71 | "name":"quality", 72 | "label":"Deadline / Quality", 73 | "default":"good", 74 | "options":[ 75 | {"name":"Good","cmds":["-quality", "good"]}, 76 | {"name":"Best","cmds":["-quality", "best"]}, 77 | {"name":"Realtime","cmds":["-quality", "realtime"]} 78 | ] 79 | }}, 80 | 81 | 82 | {"cmds":["-tune-content", "default", "-enable-tpl", "1", "-frame-parallel", "0" 83 | ,"-metadata", "Title={metadata_title}","-b:v","{br}"] 84 | }, 85 | 86 | {"conditions":[["audioChannels","contains","No audio"]], 87 | "cmds":["-an"] 88 | }, 89 | {"conditions":[["audioChannels","contains","Stereo"]], 90 | "cmds":["-ac","2","-ar","48k", "-b:a","{audoBitrate}"] 91 | }, 92 | {"conditions":[["audioChannels","contains","Mono"]], 93 | "cmds":["-ac","1","-ar","48k", "-b:a","{audoBitrate}"] 94 | }, 95 | {"conditions":[["audioChannels","contains","Copy"]], 96 | "cmds":["-c:a","copy"] 97 | }, 98 | {"cmds":["-sn"]} 99 | ] 100 | } -------------------------------------------------------------------------------- /postFilters/PostFilter-addQRCode.txt: -------------------------------------------------------------------------------- 1 | ,movie='resources/QRCode.gif'[logo],[outvpre][logo]overlay='0:0'[outv] -------------------------------------------------------------------------------- /postFilters/PostFilter-chromaShift.txt: -------------------------------------------------------------------------------- 1 | ,[outvpre]split=3[r][g][b];nullsrc=size=640x360[base1];nullsrc=size=640x360[base2];nullsrc=size=640x360[base3];[r]lutrgb=g=0:b=0[red];[g]lutrgb=r=0:b=0[green];[b]lutrgb=r=0:g=0[blue];[base1][red]overlay=x=10:shortest=1,format=rgb24[x];[base2][green]overlay=x=0:shortest=1,format=rgb24[y];[base3][blue]overlay=y=10:shortest=1,format=rgb24[z];[x][y]blend=all_mode='addition'[xy];[xy][z]blend=all_mode='addition'[xyz];[xyz]crop=630:350:10:10,scale=640:360:out_color_matrix=bt709[outv] 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /postFilters/PostFilter-mirror.txt: -------------------------------------------------------------------------------- 1 | ,[outvpre]crop=iw/2:ih:0:0,split[left][tmp];[tmp]hflip[right];[left][right]hstack[outv] -------------------------------------------------------------------------------- /postFilters/PostFilter-neonedge.txt: -------------------------------------------------------------------------------- 1 | ,[outvpre]split[v1][v2],color=red[vr],[v2]edgedetect,format=gbrp[v3],[vr][v3]scale2ref[v4][v5],[v4][v5]blend=all_mode=multiply,hue='H=2*PI*t*1:s=4',scale=1.05*iw:-1,crop=iw/1.05:ih/1.05[v6],[v1]format=gbrp[v7],[v7][v6]blend=all_mode=addition:shortest=1[outv] -------------------------------------------------------------------------------- /postFilters/PostFilter-rainbow.txt: -------------------------------------------------------------------------------- 1 | ,[outvpre]hue='H=2*PI*t*1:s=2'[outv] -------------------------------------------------------------------------------- /postFilters/PostFilter-vhsishclean.txt: -------------------------------------------------------------------------------- 1 | ,[outvpre]convolution="-2 -1 0 -1 1 1 0 1 2:-2 -1 0 -1 1 1 0 1 2:-2 -1 0 -1 1 1 0 1 2:-2 -1 0 -1 1 1 0 1 2"[outv] 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /requirements-linux.txt: -------------------------------------------------------------------------------- 1 | numpy>=1.22.0 2 | python-mpv>=0.4.6 3 | pygubu>=0.10 4 | tkinterdnd2>=0.3.0 5 | psutil 6 | pathvalidate 7 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy>=1.22.0 2 | python-mpv>=0.4.6 3 | pygubu>=0.10 4 | tkinterdnd2>=0.3.0 5 | pywin32 6 | psutil 7 | pathvalidate 8 | -------------------------------------------------------------------------------- /resources/QRCode.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfaker/WebmGenerator/e6d06e086c15367c8d3b89f27f4c9c4e245b9db4/resources/QRCode.gif -------------------------------------------------------------------------------- /resources/RankImages/Rank-0-cell.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfaker/WebmGenerator/e6d06e086c15367c8d3b89f27f4c9c4e245b9db4/resources/RankImages/Rank-0-cell.png -------------------------------------------------------------------------------- /resources/RankImages/Rank-A.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfaker/WebmGenerator/e6d06e086c15367c8d3b89f27f4c9c4e245b9db4/resources/RankImages/Rank-A.png -------------------------------------------------------------------------------- /resources/RankImages/Rank-B.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfaker/WebmGenerator/e6d06e086c15367c8d3b89f27f4c9c4e245b9db4/resources/RankImages/Rank-B.png -------------------------------------------------------------------------------- /resources/RankImages/Rank-C.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfaker/WebmGenerator/e6d06e086c15367c8d3b89f27f4c9c4e245b9db4/resources/RankImages/Rank-C.png -------------------------------------------------------------------------------- /resources/RankImages/Rank-D.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfaker/WebmGenerator/e6d06e086c15367c8d3b89f27f4c9c4e245b9db4/resources/RankImages/Rank-D.png -------------------------------------------------------------------------------- /resources/RankImages/Rank-E.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfaker/WebmGenerator/e6d06e086c15367c8d3b89f27f4c9c4e245b9db4/resources/RankImages/Rank-E.png -------------------------------------------------------------------------------- /resources/RankImages/Rank-F.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfaker/WebmGenerator/e6d06e086c15367c8d3b89f27f4c9c4e245b9db4/resources/RankImages/Rank-F.png -------------------------------------------------------------------------------- /resources/RankImages/Rank-S.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfaker/WebmGenerator/e6d06e086c15367c8d3b89f27f4c9c4e245b9db4/resources/RankImages/Rank-S.png -------------------------------------------------------------------------------- /resources/SundayMorning.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfaker/WebmGenerator/e6d06e086c15367c8d3b89f27f4c9c4e245b9db4/resources/SundayMorning.otf -------------------------------------------------------------------------------- /resources/cascade/facefinder: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfaker/WebmGenerator/e6d06e086c15367c8d3b89f27f4c9c4e245b9db4/resources/cascade/facefinder -------------------------------------------------------------------------------- /resources/cascade/lps/lp312: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfaker/WebmGenerator/e6d06e086c15367c8d3b89f27f4c9c4e245b9db4/resources/cascade/lps/lp312 -------------------------------------------------------------------------------- /resources/cascade/lps/lp38: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfaker/WebmGenerator/e6d06e086c15367c8d3b89f27f4c9c4e245b9db4/resources/cascade/lps/lp38 -------------------------------------------------------------------------------- /resources/cascade/lps/lp42: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfaker/WebmGenerator/e6d06e086c15367c8d3b89f27f4c9c4e245b9db4/resources/cascade/lps/lp42 -------------------------------------------------------------------------------- /resources/cascade/lps/lp44: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfaker/WebmGenerator/e6d06e086c15367c8d3b89f27f4c9c4e245b9db4/resources/cascade/lps/lp44 -------------------------------------------------------------------------------- /resources/cascade/lps/lp46: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfaker/WebmGenerator/e6d06e086c15367c8d3b89f27f4c9c4e245b9db4/resources/cascade/lps/lp46 -------------------------------------------------------------------------------- /resources/cascade/lps/lp81: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfaker/WebmGenerator/e6d06e086c15367c8d3b89f27f4c9c4e245b9db4/resources/cascade/lps/lp81 -------------------------------------------------------------------------------- /resources/cascade/lps/lp82: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfaker/WebmGenerator/e6d06e086c15367c8d3b89f27f4c9c4e245b9db4/resources/cascade/lps/lp82 -------------------------------------------------------------------------------- /resources/cascade/lps/lp84: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfaker/WebmGenerator/e6d06e086c15367c8d3b89f27f4c9c4e245b9db4/resources/cascade/lps/lp84 -------------------------------------------------------------------------------- /resources/cascade/lps/lp93: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfaker/WebmGenerator/e6d06e086c15367c8d3b89f27f4c9c4e245b9db4/resources/cascade/lps/lp93 -------------------------------------------------------------------------------- /resources/cascade/puploc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfaker/WebmGenerator/e6d06e086c15367c8d3b89f27f4c9c4e245b9db4/resources/cascade/puploc -------------------------------------------------------------------------------- /resources/cutPreview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfaker/WebmGenerator/e6d06e086c15367c8d3b89f27f4c9c4e245b9db4/resources/cutPreview.png -------------------------------------------------------------------------------- /resources/helptextTemplate.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Help Text 7 | 16 | 17 | 18 |
19 |
20 | 21 | 22 | 23 | 24 | 28 | 29 | 30 | 31 | 32 | 36 | 37 | 38 | 39 | 40 | 44 | 45 | 46 | 47 | 48 | 52 | 53 | 54 | 55 | 56 | 59 | 60 | 61 | 62 | 63 | 66 | 67 | 68 | 69 | 70 | 73 | 74 | 75 | 76 | 77 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 89 | 90 | 91 | 92 | 93 | 96 | 97 | 98 | 99 | 100 | 103 | 104 | 105 | 106 | 107 | 110 | 111 | 112 | 113 | 114 | 117 | 118 | 119 | 120 | 121 | 124 | 125 | 126 | 127 | 128 | 131 | 132 | 133 | 134 | 135 | 138 | 139 | 140 | 141 | 142 | 146 | 147 | 148 | 149 | 150 | 153 | 154 | 155 | 156 | 157 | 158 |
25 | Right 26 | Left 27 | Seek forwards and backwards
33 | Shift + Left 34 | Shift + Right 35 | Large seek forwards and backwards
41 | , 42 | . 43 | Single frame step
49 | Ctrl + Left 50 | Ctrl + Right 51 | Jump between subclips
57 | Space 58 | Play/Pause
64 | F 65 | Jump to random point
71 | Y 72 | Accept subclip preview, Jump to random point and add new subclip preview
78 | U 79 | Jump to random point and add new subclip preview
87 | V 88 | Start/End a new selection at the current time
94 | B 95 | Add a new subclip at the current time
101 | C 102 | Cut/Slice current sublip into two at current time
108 | D 109 | Delete Subclip at current time
115 | M 116 | Merge all subclips under current selection
122 | Ctrl + F 123 | Jump to next file matching text search
129 | Ctrl + R 130 | Jump to random file matching text search
136 | Ctrl + A 137 | Select whole video as new subclip
143 | Q 144 | E 145 | Jump to next/previous file
151 | R 152 | Jump to random file
159 |
160 |
161 | 162 | 163 | -------------------------------------------------------------------------------- /resources/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfaker/WebmGenerator/e6d06e086c15367c8d3b89f27f4c9c4e245b9db4/resources/icon.ico -------------------------------------------------------------------------------- /resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfaker/WebmGenerator/e6d06e086c15367c8d3b89f27f4c9c4e245b9db4/resources/icon.png -------------------------------------------------------------------------------- /resources/icons/alphabet-s.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfaker/WebmGenerator/e6d06e086c15367c8d3b89f27f4c9c4e245b9db4/resources/icons/alphabet-s.png -------------------------------------------------------------------------------- /resources/icons/chevrons-right-arrows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfaker/WebmGenerator/e6d06e086c15367c8d3b89f27f4c9c4e245b9db4/resources/icons/chevrons-right-arrows.png -------------------------------------------------------------------------------- /resources/icons/clock-time.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfaker/WebmGenerator/e6d06e086c15367c8d3b89f27f4c9c4e245b9db4/resources/icons/clock-time.png -------------------------------------------------------------------------------- /resources/icons/copy-duplicate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfaker/WebmGenerator/e6d06e086c15367c8d3b89f27f4c9c4e245b9db4/resources/icons/copy-duplicate.png -------------------------------------------------------------------------------- /resources/icons/folder-add-file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfaker/WebmGenerator/e6d06e086c15367c8d3b89f27f4c9c4e245b9db4/resources/icons/folder-add-file.png -------------------------------------------------------------------------------- /resources/icons/folder-file-project.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfaker/WebmGenerator/e6d06e086c15367c8d3b89f27f4c9c4e245b9db4/resources/icons/folder-file-project.png -------------------------------------------------------------------------------- /resources/icons/maximize-expand.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfaker/WebmGenerator/e6d06e086c15367c8d3b89f27f4c9c4e245b9db4/resources/icons/maximize-expand.png -------------------------------------------------------------------------------- /resources/icons/media-film-video.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfaker/WebmGenerator/e6d06e086c15367c8d3b89f27f4c9c4e245b9db4/resources/icons/media-film-video.png -------------------------------------------------------------------------------- /resources/icons/more-horizontal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfaker/WebmGenerator/e6d06e086c15367c8d3b89f27f4c9c4e245b9db4/resources/icons/more-horizontal.png -------------------------------------------------------------------------------- /resources/icons/photo-image-picture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfaker/WebmGenerator/e6d06e086c15367c8d3b89f27f4c9c4e245b9db4/resources/icons/photo-image-picture.png -------------------------------------------------------------------------------- /resources/icons/redo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfaker/WebmGenerator/e6d06e086c15367c8d3b89f27f4c9c4e245b9db4/resources/icons/redo.png -------------------------------------------------------------------------------- /resources/icons/save-alt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfaker/WebmGenerator/e6d06e086c15367c8d3b89f27f4c9c4e245b9db4/resources/icons/save-alt.png -------------------------------------------------------------------------------- /resources/icons/save.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfaker/WebmGenerator/e6d06e086c15367c8d3b89f27f4c9c4e245b9db4/resources/icons/save.png -------------------------------------------------------------------------------- /resources/icons/video-camera-media.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfaker/WebmGenerator/e6d06e086c15367c8d3b89f27f4c9c4e245b9db4/resources/icons/video-camera-media.png -------------------------------------------------------------------------------- /resources/icons/youtube-video.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfaker/WebmGenerator/e6d06e086c15367c8d3b89f27f4c9c4e245b9db4/resources/icons/youtube-video.png -------------------------------------------------------------------------------- /resources/loadingPreview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfaker/WebmGenerator/e6d06e086c15367c8d3b89f27f4c9c4e245b9db4/resources/loadingPreview.png -------------------------------------------------------------------------------- /resources/playerbg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfaker/WebmGenerator/e6d06e086c15367c8d3b89f27f4c9c4e245b9db4/resources/playerbg.png -------------------------------------------------------------------------------- /resources/quicksand.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfaker/WebmGenerator/e6d06e086c15367c8d3b89f27f4c9c4e245b9db4/resources/quicksand.otf -------------------------------------------------------------------------------- /resources/slider_left_base.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfaker/WebmGenerator/e6d06e086c15367c8d3b89f27f4c9c4e245b9db4/resources/slider_left_base.gif -------------------------------------------------------------------------------- /resources/slider_left_light.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfaker/WebmGenerator/e6d06e086c15367c8d3b89f27f4c9c4e245b9db4/resources/slider_left_light.gif -------------------------------------------------------------------------------- /resources/slider_right_base.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfaker/WebmGenerator/e6d06e086c15367c8d3b89f27f4c9c4e245b9db4/resources/slider_right_base.gif -------------------------------------------------------------------------------- /resources/slider_right_light.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfaker/WebmGenerator/e6d06e086c15367c8d3b89f27f4c9c4e245b9db4/resources/slider_right_light.gif -------------------------------------------------------------------------------- /resources/sliders.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfaker/WebmGenerator/e6d06e086c15367c8d3b89f27f4c9c4e245b9db4/resources/sliders.png -------------------------------------------------------------------------------- /resources/speechModel/weights.hdf5.xz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfaker/WebmGenerator/e6d06e086c15367c8d3b89f27f4c9c4e245b9db4/resources/speechModel/weights.hdf5.xz -------------------------------------------------------------------------------- /resources/voiceModel/weights.hdf5.xz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dfaker/WebmGenerator/e6d06e086c15367c8d3b89f27f4c9c4e245b9db4/resources/voiceModel/weights.hdf5.xz -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from cx_Freeze import setup, Executable 2 | import os 3 | 4 | PYTHON_INSTALL_DIR = os.path.dirname(os.path.dirname(os.__file__)) 5 | 6 | os.environ['TCL_LIBRARY'] = os.path.join(PYTHON_INSTALL_DIR, 'tcl', 'tcl8.6') 7 | os.environ['TK_LIBRARY'] = os.path.join(PYTHON_INSTALL_DIR, 'tcl', 'tk8.6') 8 | 9 | # Dependencies are automatically detected, but it might need 10 | # fine tuning. 11 | 12 | buildVersion = None 13 | 14 | for line in open(os.path.join('src','webmGeneratorUi.py') ).readlines(): 15 | if 'RELEASE_NUMVER = ' in line: 16 | buildVersion = line.split("'")[1] 17 | if buildVersion is not None and 'v' in buildVersion: 18 | print('buildVersion=',buildVersion) 19 | else: 20 | print('buildVersion invalid=',buildVersion) 21 | 22 | buildOptions = dict(packages = ["os"], 23 | include_files = [ 24 | ('resources\\QRCode.gif','resources\\QRCode.gif') 25 | ,('resources\\quicksand.otf','resources\\quicksand.otf') 26 | ,('resources\\playerbg.png','resources\\playerbg.png') 27 | 28 | ,('filterTemplates\\4 Second Header Text.json','filterTemplates\\4 Second Header Text.json') 29 | 30 | ,('resources\\RankImages\\Rank-0-cell.png','resources\\RankImages\\Rank-0-cell.png') 31 | ,('resources\\RankImages\\Rank-A.png','resources\\RankImages\\Rank-A.png') 32 | ,('resources\\RankImages\\Rank-B.png','resources\\RankImages\\Rank-B.png') 33 | ,('resources\\RankImages\\Rank-C.png','resources\\RankImages\\Rank-C.png') 34 | ,('resources\\RankImages\\Rank-D.png','resources\\RankImages\\Rank-D.png') 35 | ,('resources\\RankImages\\Rank-E.png','resources\\RankImages\\Rank-E.png') 36 | ,('resources\\RankImages\\Rank-F.png','resources\\RankImages\\Rank-F.png') 37 | 38 | ,('resources\\slider_right_light.gif','resources\\slider_right_light.gif') 39 | ,('resources\\slider_left_light.gif','resources\\slider_left_light.gif') 40 | 41 | ,('resources\\slider_right_light.gif','resources\\slider_right_base.gif') 42 | ,('resources\\slider_left_light.gif','resources\\slider_left_base.gif') 43 | 44 | ,('resources\\cutPreview.png','resources\\cutPreview.png') 45 | ,('resources\\loadingPreview.png','resources\\loadingPreview.png') 46 | 47 | ,('resources\\icons\\','resources\\icons\\') 48 | ,('resources\\cascade\\','resources\\cascade\\') 49 | ,('resources\\speechModel\\','resources\\speechModel\\') 50 | ,('resources\\voiceModel\\','resources\\voiceModel\\') 51 | 52 | ,('postFilters\\','postFilters\\') 53 | ,('customEncodeprofiles\\','customEncodeprofiles\\') 54 | ,('customEnoderSpecs\\','customEnoderSpecs\\') 55 | 56 | ,('src\\screenspacetools.lua','src\\screenspacetools.lua') 57 | ,('src\\vrscript.lua','src\\vrscript.lua') 58 | 59 | ,('src\\filterSpecs.json','src\\filterSpecs.json') 60 | 61 | ,'yt-dlp.exe' 62 | ,'ffmpeg.exe' 63 | ,'ffprobe.exe' 64 | ,'mpv-2.dll' 65 | ,os.path.join(PYTHON_INSTALL_DIR, 'DLLs', 'tk86t.dll') 66 | ,os.path.join(PYTHON_INSTALL_DIR, 'DLLs', 'tcl86t.dll') 67 | ], 68 | includes= ["tkinter","email","http","tkinter.ttk","secrets"], 69 | excludes = [ 70 | 'PIL', 71 | 'distutils', 72 | 'future', 73 | 'pydoc_data', 74 | 'setuptools', 75 | 'test', 76 | 'tests', 77 | 'test', 78 | "colorama", 79 | "curses", 80 | "email", 81 | "jinja2", 82 | "markupsafe", 83 | "scipy", 84 | "numba", 85 | "numpy.core._dotblas", 86 | "PIL", 87 | "pycparser", 88 | "PyQt4.QtNetwork", 89 | "PyQt4.QtScript", 90 | "PyQt4.QtSql", 91 | "PyQt5", 92 | "pytz", 93 | "scipy.lib.lapack.flapack", 94 | "sqlite3", 95 | "test", 96 | 'dbm', 97 | 'http', 98 | 'llvmlite', 99 | 'matplotlib', 100 | 'sklearn', 101 | 'multiprocessing', 102 | 'test', 103 | 'unittest', 104 | 'xmlrpc', 105 | ]) 106 | 107 | 108 | base = "console" 109 | 110 | executables = [ 111 | Executable('webmGenerator.py', base=base, icon = 'resources\\icon.ico') 112 | ] 113 | 114 | setup(name='WebmGenerator', 115 | version = '1.1', 116 | description = 'UI and Automation to generate high quality VP8 webms', 117 | options = dict(build_exe = buildOptions), 118 | executables = executables) 119 | 120 | # The Uppercase name is detected first in a failed Queue import and 121 | # doesn't get lowercased again (possibly only with case insensitive paths) 122 | # this renames Tkinter->tkinter. 123 | import os 124 | import glob 125 | for tkinterfolder in glob.glob(os.path.join('build','*','lib','Tkinter')): 126 | os.rename(tkinterfolder,tkinterfolder.replace('Tkinter','tkinter')) 127 | 128 | 129 | for existingArch in glob.glob(os.path.join('build','*','*.zip')): 130 | os.remove(existingArch) 131 | 132 | for buildPath in glob.glob(os.path.join('build','*')): 133 | os.chdir(buildPath) 134 | finalZipLocation = os.path.join('.','WebmGenerator-win64-{}.zip'.format(buildVersion)) 135 | print(finalZipLocation) 136 | print(buildPath) 137 | z7cmd = '"C:\\Program Files\\7-Zip\\7z" a -mm=Deflate -mfb=258 -mpass=15 -r {} {}'.format(finalZipLocation,'.') 138 | print(z7cmd) 139 | os.system(z7cmd) 140 | break 141 | -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/composeController.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import json 4 | 5 | class ComposeController: 6 | 7 | def __init__(self,ui,videoManager,ffmpegService,filterController,globalOptions={}): 8 | self.ui=ui 9 | self.globalOptions=globalOptions 10 | self.videoManager=videoManager 11 | self.ffmpegService=ffmpegService 12 | self.filterController=filterController 13 | 14 | self.ui.setController(self) 15 | 16 | if __name__ == '__main__': 17 | import webmGenerator 18 | -------------------------------------------------------------------------------- /src/composeUi.py: -------------------------------------------------------------------------------- 1 | import tkinter as tk 2 | import tkinter.ttk as ttk 3 | from pygubu.widgets.scrolledframe import ScrolledFrame 4 | import os 5 | import string 6 | import mpv 7 | from tkinter.filedialog import askopenfilename 8 | import random 9 | import time 10 | from collections import deque 11 | import logging 12 | import json 13 | import threading 14 | 15 | 16 | 17 | class ComposeUi(ttk.Frame): 18 | 19 | def __init__(self, master=None,defaultProfile='None', *args, **kwargs): 20 | ttk.Frame.__init__(self, master) 21 | 22 | self.master=master 23 | self.controller=None 24 | self.defaultProfile=defaultProfile 25 | 26 | def setController(self,controller): 27 | self.controller=controller 28 | 29 | def tabSwitched(self,tabName): 30 | pass 31 | 32 | 33 | if __name__ == '__main__': 34 | import webmGenerator 35 | -------------------------------------------------------------------------------- /src/encoders/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/encoders/apngEncoder.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import logging 4 | import subprocess as sp 5 | 6 | from ..encodingUtils import getFreeNameForFileAndLog 7 | from ..encodingUtils import logffmpegEncodeProgress 8 | from ..encodingUtils import isRquestCancelled 9 | 10 | from ..optimisers.nelderMead import encodeTargetingSize as encodeTargetingSize_nelder_mead 11 | from ..optimisers.linear import encodeTargetingSize as encodeTargetingSize_linear 12 | 13 | def encoder(inputsList, outputPathName,filenamePrefix, filtercommand, options, totalEncodedSeconds, totalExpectedEncodedSeconds, statusCallback,encodeStageFilter='null',requestId=None,globalOptions={},packageglobalStatusCallback=print,startEndTimestamps=None): 14 | 15 | if options.get('maximumSize') == 0.0: 16 | sizeLimitMax = float('inf') 17 | sizeLimitMin = float('-inf') 18 | else: 19 | sizeLimitMax = options.get('maximumSize')*1024*1024 20 | sizeLimitMin = sizeLimitMax*(1.0-globalOptions.get('allowableTargetSizeUnderrun',0.25)) 21 | 22 | videoFileName,logFilePath,filterFilePath,tempVideoFilePath,videoFilePath = getFreeNameForFileAndLog(filenamePrefix, 'png', requestId) 23 | 24 | def encoderStatusCallback(text,percentage,**kwargs): 25 | statusCallback(text,percentage,**kwargs) 26 | packageglobalStatusCallback(text,percentage) 27 | 28 | def encoderFunction(width,passNumber,passReason,passPhase=0,requestId=None,widthReduction=0.0,bufsize=None, cqMode=False): 29 | 30 | giffiltercommand = filtercommand+',[outv]scale=w=iw*sar:h=ih,setsar=sar=1/1,scale=\'max({}\\,min({}\\,iw)):-1\':flags=area[outvgif],[outa]anullsink'.format(0,width) 31 | 32 | with open(filterFilePath,'wb') as filterFile: 33 | filterFile.write(giffiltercommand.encode('utf8')) 34 | 35 | ffmpegcommand=[] 36 | ffmpegcommand+=['ffmpeg' ,'-y'] 37 | 38 | s,e = None,None 39 | if startEndTimestamps is not None: 40 | s,e = startEndTimestamps 41 | ffmpegcommand+=['-ss', str(s)] 42 | ffmpegcommand+=inputsList 43 | ffmpegcommand+=['-to', str(e-s)] 44 | else: 45 | ffmpegcommand+=inputsList 46 | 47 | ffmpegcommand+=['-plays', '0'] 48 | ffmpegcommand+=['-filter_complex_script',filterFilePath] 49 | ffmpegcommand+=['-map','[outvgif]'] 50 | 51 | 52 | if startEndTimestamps is None: 53 | ffmpegcommand+=["-shortest" 54 | ,"-copyts" 55 | ,"-start_at_zero"] 56 | 57 | ffmpegcommand+=["-vsync", 'passthrough' 58 | ,"-stats" 59 | ,"-an" 60 | ,'-flags','+psnr' 61 | ,"-f","apng" 62 | ,"-sn",tempVideoFilePath] 63 | 64 | encoderStatusCallback('Encoding final '+videoFileName,(totalEncodedSeconds)/totalExpectedEncodedSeconds) 65 | 66 | proc = sp.Popen(ffmpegcommand,stderr=sp.PIPE,stdin=sp.DEVNULL,stdout=sp.DEVNULL) 67 | psnr, returnCode = logffmpegEncodeProgress(proc,'Pass {} {} {}'.format(passNumber,passReason,videoFileName),totalEncodedSeconds,totalExpectedEncodedSeconds,encoderStatusCallback,passNumber=0,requestId=requestId,tempVideoPath=tempVideoFilePath,options=options) 68 | if isRquestCancelled(requestId): 69 | return 0, psnr, returnCode 70 | finalSize = os.stat(tempVideoFilePath).st_size 71 | encoderStatusCallback(None,None,lastEncodedSize=finalSize) 72 | return finalSize, psnr, returnCode 73 | 74 | initialWidth = options.get('maximumWidth',1280) 75 | 76 | encoderFunction.supportsCRQMode=False 77 | optimiser = encodeTargetingSize_linear 78 | if 'Nelder-Mead' in options.get('optimizer'): 79 | optimiser = encodeTargetingSize_nelder_mead 80 | 81 | finalFilenameConfirmed = optimiser(encoderFunction=encoderFunction, 82 | tempFilename=tempVideoFilePath, 83 | outputFilename=videoFilePath, 84 | initialDependentValue=initialWidth, 85 | sizeLimitMin=sizeLimitMin, 86 | sizeLimitMax=sizeLimitMax, 87 | allowEarlyExitWhenUndersize=globalOptions.get('allowEarlyExitIfUndersized',True), 88 | maxAttempts=globalOptions.get('maxEncodeAttemptsGif',10), 89 | dependentValueName='Width', 90 | dependentValueMaximum=options.get('maximumWidth',0), 91 | requestId=requestId, 92 | optimiserName=options.get('optimizer'), 93 | globalOptions=globalOptions) 94 | 95 | encoderStatusCallback('Encoding final '+videoFileName,(totalEncodedSeconds)/totalExpectedEncodedSeconds ) 96 | encoderStatusCallback('Encoding complete '+videoFilePath,1,finalFilename=finalFilenameConfirmed) -------------------------------------------------------------------------------- /src/encoders/gifEncoder.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import logging 4 | import subprocess as sp 5 | 6 | from ..ffmpegInfoParser import getVideoInfo 7 | 8 | from math import sqrt 9 | 10 | from ..encodingUtils import getFreeNameForFileAndLog 11 | from ..encodingUtils import logffmpegEncodeProgress 12 | from ..encodingUtils import isRquestCancelled 13 | 14 | from ..optimisers.nelderMead import encodeTargetingSize as encodeTargetingSize_nelder_mead 15 | from ..optimisers.linear import encodeTargetingSize as encodeTargetingSize_linear 16 | 17 | def encoder(inputsList, outputPathName,filenamePrefix, filtercommand, options, totalEncodedSeconds, totalExpectedEncodedSeconds, statusCallback,encodeStageFilter='null',requestId=None,globalOptions={},packageglobalStatusCallback=print,startEndTimestamps=None): 18 | 19 | if options.get('maximumSize') == 0.0: 20 | sizeLimitMax = float('inf') 21 | sizeLimitMin = float('-inf') 22 | else: 23 | sizeLimitMax = options.get('maximumSize')*1024*1024 24 | sizeLimitMin = sizeLimitMax*(1.0-globalOptions.get('allowableTargetSizeUnderrun',0.25)) 25 | 26 | videoFileName,logFilePath,filterFilePath,tempVideoFilePath,videoFilePath = getFreeNameForFileAndLog(filenamePrefix, 'gif', requestId) 27 | 28 | def encoderStatusCallback(text,percentage,**kwargs): 29 | statusCallback(text,percentage,**kwargs) 30 | packageglobalStatusCallback(text,percentage) 31 | 32 | def encoderFunction(width,passNumber,passReason,passPhase=0,requestId=None,widthReduction=0.0,bufsize=None, cqMode=False): 33 | 34 | 35 | gifFPSLimit='' 36 | if options.get('forceGifFPS',True): 37 | gifFPSLimit='fps=18,' 38 | 39 | giffiltercommand = filtercommand+',[outv]scale=w=iw*sar:h=ih,setsar=sar=1/1,scale=\'max({}\\,min({}\\,iw)):-1\':flags=area,{}split[pal1][outvpal],[pal1]palettegen=stats_mode=diff[plt],[outvpal][plt]paletteuse=dither=floyd_steinberg:[outvgif],[outa]anullsink'.format(0,width,gifFPSLimit) 40 | 41 | 42 | with open(filterFilePath,'wb') as filterFile: 43 | filterFile.write(giffiltercommand.encode('utf8')) 44 | 45 | ffmpegcommand=[] 46 | ffmpegcommand+=['ffmpeg' ,'-y'] 47 | 48 | 49 | s,e = None,None 50 | if startEndTimestamps is not None: 51 | s,e = startEndTimestamps 52 | ffmpegcommand+=['-ss', str(s)] 53 | ffmpegcommand+=inputsList 54 | ffmpegcommand+=['-to', str(e-s)] 55 | else: 56 | ffmpegcommand+=inputsList 57 | 58 | ffmpegcommand+=['-filter_complex_script',filterFilePath] 59 | ffmpegcommand+=['-map','[outvgif]'] 60 | 61 | if startEndTimestamps is None: 62 | ffmpegcommand+=["-shortest" 63 | ,"-copyts" 64 | ,"-start_at_zero"] 65 | 66 | 67 | ffmpegcommand+=["-vsync", 'passthrough' 68 | ,"-stats" 69 | ,"-an" 70 | ,'-flags','+psnr' 71 | ,"-sn",tempVideoFilePath] 72 | 73 | encoderStatusCallback('Encoding final '+videoFileName,(totalEncodedSeconds)/totalExpectedEncodedSeconds) 74 | 75 | proc = sp.Popen(ffmpegcommand,stderr=sp.PIPE,stdin=sp.DEVNULL,stdout=sp.DEVNULL) 76 | psnr, returnCode = logffmpegEncodeProgress(proc,'Pass {} {} {}'.format(passNumber,passReason,videoFileName),totalEncodedSeconds,totalExpectedEncodedSeconds,encoderStatusCallback,passNumber=0,requestId=requestId,tempVideoPath=tempVideoFilePath,options=options) 77 | if isRquestCancelled(requestId): 78 | return 0, psnr, returnCode 79 | finalSize = os.stat(tempVideoFilePath).st_size 80 | encoderStatusCallback(None,None,lastEncodedSize=finalSize) 81 | return finalSize, psnr, returnCode 82 | 83 | initialWidth = options.get('maximumWidth',1280) 84 | 85 | print('initialWidth',initialWidth) 86 | print('inputsList',inputsList) 87 | try: 88 | if len(inputsList) == 2: 89 | vi = getVideoInfo(inputsList[1]) 90 | 91 | if options.get('forceGifFPS',True): 92 | vi.fps = 18 93 | 94 | area = sizeLimitMax/(vi.duration*vi.fps) 95 | print('area',area) 96 | tw = int(sqrt(area*(vi.width/vi.height))) 97 | th = int(sqrt(area*(vi.height/vi.width))) 98 | initialWidth = int(max(tw,th)*1.2) 99 | print('TARGET WIDTH',initialWidth) 100 | except Exception as e: 101 | print('TARGET WIDTH Exception',e) 102 | 103 | encoderFunction.supportsCRQMode=False 104 | optimiser = encodeTargetingSize_linear 105 | if 'Nelder-Mead' in options.get('optimizer'): 106 | optimiser = encodeTargetingSize_nelder_mead 107 | 108 | finalFilenameConfirmed = optimiser(encoderFunction=encoderFunction, 109 | tempFilename=tempVideoFilePath, 110 | outputFilename=videoFilePath, 111 | initialDependentValue=initialWidth, 112 | sizeLimitMin=sizeLimitMin, 113 | sizeLimitMax=sizeLimitMax, 114 | allowEarlyExitWhenUndersize=globalOptions.get('allowEarlyExitIfUndersized',True), 115 | maxAttempts=globalOptions.get('maxEncodeAttemptsGif',10), 116 | dependentValueName='Width', 117 | dependentValueMaximum=options.get('maximumWidth',0), 118 | requestId=requestId, 119 | optimiserName=options.get('optimizer'), 120 | globalOptions=globalOptions) 121 | 122 | encoderStatusCallback('Encoding final '+videoFileName,(totalEncodedSeconds)/totalExpectedEncodedSeconds ) 123 | encoderStatusCallback('Encoding complete '+videoFilePath,1,finalFilename=finalFilenameConfirmed) -------------------------------------------------------------------------------- /src/encoders/gifskiEncoder.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import logging 4 | import subprocess as sp 5 | 6 | from ..ffmpegInfoParser import getVideoInfo 7 | 8 | from math import sqrt 9 | 10 | from ..encodingUtils import getFreeNameForFileAndLog 11 | from ..encodingUtils import logffmpegEncodeProgress 12 | from ..encodingUtils import isRquestCancelled 13 | 14 | from ..optimisers.nelderMead import encodeTargetingSize as encodeTargetingSize_nelder_mead 15 | from ..optimisers.linear import encodeTargetingSize as encodeTargetingSize_linear 16 | 17 | def encoder(inputsList, outputPathName,filenamePrefix, filtercommand, options, totalEncodedSeconds, totalExpectedEncodedSeconds, statusCallback,encodeStageFilter='null',requestId=None,globalOptions={},packageglobalStatusCallback=print,startEndTimestamps=None): 18 | 19 | if options.get('maximumSize') == 0.0: 20 | sizeLimitMax = float('inf') 21 | sizeLimitMin = float('-inf') 22 | else: 23 | sizeLimitMax = options.get('maximumSize')*1024*1024 24 | sizeLimitMin = sizeLimitMax*(1.0-globalOptions.get('allowableTargetSizeUnderrun',0.25)) 25 | 26 | intervideoFileName,interlogFilePath, interfilterFilePath, intertempVideoFilePath, intervideoFilePath = getFreeNameForFileAndLog(filenamePrefix, 'mp4', requestId) 27 | videoFileName,logFilePath,filterFilePath,tempVideoFilePath,videoFilePath = getFreeNameForFileAndLog(filenamePrefix, 'gif', requestId) 28 | 29 | def encoderStatusCallback(text,percentage,**kwargs): 30 | statusCallback(text,percentage,**kwargs) 31 | packageglobalStatusCallback(text,percentage) 32 | 33 | def encoderFunction(width,passNumber,passReason,passPhase=0,requestId=None,widthReduction=0.0,bufsize=None, cqMode=False): 34 | 35 | 36 | gifFPSLimit='' 37 | if options.get('forceGifFPS',True): 38 | gifFPSLimit='fps=18,' 39 | 40 | giffiltercommand = filtercommand+',[outv]scale=w=iw*sar:h=ih,setsar=sar=1/1[outvgif],[outa]anullsink' 41 | 42 | 43 | with open(interfilterFilePath,'wb') as filterFile: 44 | filterFile.write(giffiltercommand.encode('utf8')) 45 | 46 | ffmpegcommand=[] 47 | ffmpegcommand+=['ffmpeg' ,'-y'] 48 | 49 | 50 | s,e = None,None 51 | if startEndTimestamps is not None: 52 | s,e = startEndTimestamps 53 | ffmpegcommand+=['-ss', str(s)] 54 | ffmpegcommand+=inputsList 55 | ffmpegcommand+=['-to', str(e-s)] 56 | else: 57 | ffmpegcommand+=inputsList 58 | 59 | ffmpegcommand+=['-filter_complex_script',interfilterFilePath] 60 | ffmpegcommand+=['-map','[outvgif]'] 61 | ffmpegcommand+=['-pix_fmt', 'yuv444p' 62 | ,'-f', "yuv4mpegpipe" 63 | ,'-strict', '-1' 64 | ,"-sn",'-'] 65 | 66 | encoderStatusCallback('Encoding final '+videoFileName,(totalEncodedSeconds)/totalExpectedEncodedSeconds) 67 | 68 | proc = sp.Popen(ffmpegcommand,stderr=sp.DEVNULL,stdin=sp.DEVNULL,stdout=sp.PIPE) 69 | proc2 = sp.Popen(['gifski', '-o', tempVideoFilePath, '--extra', '--width', str(width), '-'],stdin=sp.PIPE) 70 | 71 | psnr, returnCode = logffmpegEncodeProgress(proc,'Pass {} {} {}'.format(passNumber,passReason,videoFileName),totalEncodedSeconds,totalExpectedEncodedSeconds,encoderStatusCallback,framesink=proc2,passNumber=0,requestId=requestId,tempVideoPath=tempVideoFilePath,options=options) 72 | 73 | encoderStatusCallback('Encoding gifski '+videoFileName,0.80) 74 | 75 | 76 | encoderStatusCallback('Encoding gifski final'+videoFileName,0.9) 77 | 78 | psnr,returnCode = 100,0 79 | 80 | if isRquestCancelled(requestId): 81 | return 0, psnr, returnCode 82 | 83 | finalSize = os.stat(tempVideoFilePath).st_size 84 | encoderStatusCallback(None,None,lastEncodedSize=finalSize) 85 | 86 | encoderStatusCallback('Encoding gifski final'+videoFileName,0.95) 87 | 88 | return finalSize, psnr, returnCode 89 | 90 | initialWidth = options.get('maximumWidth',1280) 91 | 92 | print('initialWidth',initialWidth) 93 | print('inputsList',inputsList) 94 | try: 95 | if len(inputsList) == 2: 96 | vi = getVideoInfo(inputsList[1]) 97 | 98 | if options.get('forceGifFPS',True): 99 | vi.fps = 18 100 | 101 | area = sizeLimitMax/(vi.duration*vi.fps) 102 | print('area',area) 103 | tw = int(sqrt(area*(vi.width/vi.height))) 104 | th = int(sqrt(area*(vi.height/vi.width))) 105 | initialWidth = int(max(tw,th)*1.2) 106 | print('TARGET WIDTH',initialWidth) 107 | except Exception as e: 108 | print('TARGET WIDTH Exception',e) 109 | 110 | encoderFunction.supportsCRQMode=False 111 | optimiser = encodeTargetingSize_linear 112 | if 'Nelder-Mead' in options.get('optimizer'): 113 | optimiser = encodeTargetingSize_nelder_mead 114 | 115 | finalFilenameConfirmed = optimiser(encoderFunction=encoderFunction, 116 | tempFilename=tempVideoFilePath, 117 | outputFilename=videoFilePath, 118 | initialDependentValue=initialWidth, 119 | sizeLimitMin=sizeLimitMin, 120 | sizeLimitMax=sizeLimitMax, 121 | allowEarlyExitWhenUndersize=globalOptions.get('allowEarlyExitIfUndersized',True), 122 | maxAttempts=globalOptions.get('maxEncodeAttemptsGif',10), 123 | dependentValueName='Width', 124 | dependentValueMaximum=options.get('maximumWidth',0), 125 | requestId=requestId, 126 | optimiserName=options.get('optimizer'), 127 | globalOptions=globalOptions) 128 | 129 | encoderStatusCallback('Encoding final '+videoFileName,(totalEncodedSeconds)/totalExpectedEncodedSeconds ) 130 | encoderStatusCallback('Encoding complete '+videoFilePath,1,finalFilename=finalFilenameConfirmed) -------------------------------------------------------------------------------- /src/encoders/mp4AV1Encoder.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import logging 4 | import subprocess as sp 5 | 6 | from ..encodingUtils import getFreeNameForFileAndLog 7 | from ..encodingUtils import logffmpegEncodeProgress 8 | from ..encodingUtils import isRquestCancelled 9 | 10 | from ..optimisers.nelderMead import encodeTargetingSize as encodeTargetingSize_nelder_mead 11 | from ..optimisers.linear import encodeTargetingSize as encodeTargetingSize_linear 12 | 13 | def encoder(inputsList, outputPathName,filenamePrefix, filtercommand, options, totalEncodedSeconds, totalExpectedEncodedSeconds, statusCallback,requestId=None,encodeStageFilter='null',globalOptions={},packageglobalStatusCallback=print,startEndTimestamps=None): 14 | 15 | audoBitrate = 8 16 | try: 17 | audoBitrate = int(float(options.get('audioRate','8'))) 18 | except Exception as e: 19 | print(e) 20 | 21 | audoBitrate = int(audoBitrate)*1024 22 | 23 | 24 | audio_mp = 8 25 | video_mp = 1024*1024 26 | initialBr = globalOptions.get('initialBr',16777216) 27 | dur = totalExpectedEncodedSeconds-totalEncodedSeconds 28 | 29 | if options.get('maximumSize') == 0.0: 30 | sizeLimitMax = float('inf') 31 | sizeLimitMin = float('-inf') 32 | initialBr = globalOptions.get('initialBr',16777216) 33 | else: 34 | sizeLimitMax = options.get('maximumSize')*1024*1024 35 | sizeLimitMin = sizeLimitMax*(1.0-globalOptions.get('allowableTargetSizeUnderrun',0.25)) 36 | targetSize_guide = (sizeLimitMin+sizeLimitMax)/2 37 | initialBr = ( ((targetSize_guide)/dur) - ((audoBitrate / 1024 / audio_mp)/dur) )*8 38 | 39 | videoFileName,logFilePath,filterFilePath,tempVideoFilePath,videoFilePath = getFreeNameForFileAndLog(filenamePrefix, 'mp4', requestId) 40 | 41 | def encoderStatusCallback(text,percentage,**kwargs): 42 | statusCallback(text,percentage,**kwargs) 43 | packageglobalStatusCallback(text,percentage) 44 | 45 | def encoderFunction(br,passNumber,passReason,passPhase=0, requestId=None,widthReduction=0.0,bufsize=None, cqMode=False): 46 | 47 | ffmpegcommand=[] 48 | ffmpegcommand+=['ffmpeg' ,'-y'] 49 | 50 | 51 | 52 | s,e = None,None 53 | if startEndTimestamps is not None: 54 | s,e = startEndTimestamps 55 | ffmpegcommand+=['-ss', str(s)] 56 | ffmpegcommand+=inputsList 57 | ffmpegcommand+=['-to', str(e-s)] 58 | else: 59 | ffmpegcommand+=inputsList 60 | 61 | 62 | 63 | if widthReduction>0.0: 64 | encodefiltercommand = filtercommand+',{encodeStageFilter},[outv]scale=iw*(1-{widthReduction}):ih*(1-{widthReduction}):flags=bicubic[outvfinal]'.format(widthReduction=widthReduction,encodeStageFilter=encodeStageFilter) 65 | else: 66 | encodefiltercommand = filtercommand+',[outv]null,{encodeStageFilter}[outvfinal]'.format(encodeStageFilter=encodeStageFilter) 67 | 68 | if options.get('audioChannels') == 'No audio': 69 | encodefiltercommand+=',[outa]anullsink' 70 | 71 | with open(filterFilePath,'wb') as filterFile: 72 | filterFile.write(encodefiltercommand.encode('utf8')) 73 | 74 | if options.get('audioChannels') == 'No audio': 75 | ffmpegcommand+=['-filter_complex_script',filterFilePath] 76 | ffmpegcommand+=['-map','[outvfinal]'] 77 | elif 'Copy' in options.get('audioChannels',''): 78 | ffmpegcommand+=['-filter_complex_script',filterFilePath] 79 | ffmpegcommand+=['-map','[outvfinal]','-map','a:0'] 80 | else: 81 | ffmpegcommand+=['-filter_complex_script',filterFilePath] 82 | ffmpegcommand+=['-map','[outvfinal]','-map','[outa]'] 83 | 84 | 85 | if passPhase==1: 86 | ffmpegcommand+=['-pass', '1', '-passlogfile', logFilePath ] 87 | elif passPhase==2: 88 | ffmpegcommand+=['-pass', '2', '-passlogfile', logFilePath ] 89 | 90 | if bufsize is None: 91 | bufsize = 3000000 92 | if sizeLimitMax != 0.0: 93 | bufsize = str(min(2000000000.0,br*2)) 94 | 95 | threadCount = globalOptions.get('encoderStageThreads',4) 96 | metadataSuffix = globalOptions.get('titleMetadataSuffix',' WmG') 97 | 98 | 99 | audioCodec = ["-c:a","libopus"] 100 | if 'Copy' in options.get('audioChannels',''): 101 | audioCodec = [] 102 | 103 | presetNum = 8 104 | try: 105 | presetNum = options.get('svtav1Preset',8) 106 | except Exception as e: 107 | print(e) 108 | 109 | if startEndTimestamps is None: 110 | ffmpegcommand+=["-shortest" 111 | ,"-copyts" 112 | ,"-start_at_zero"] 113 | 114 | 115 | ffmpegcommand+=["-slices", "8" 116 | ,"-c:v","libsvtav1"] + audioCodec + [ 117 | "-stats","-pix_fmt","yuv420p","-bufsize", str(bufsize) 118 | ,"-threads", str(threadCount),"-crf" ,'25','-g', '300' 119 | ,'-flags','+psnr','-cpu-used','0','-row-mt', '1', '-preset', str(presetNum) 120 | ,"-metadata", 'title={}'.format(filenamePrefix.replace('-',' -') + metadataSuffix) ] 121 | 122 | if sizeLimitMax == 0.0: 123 | ffmpegcommand+=["-b:v","0","-qmin","0","-qmax","10"] 124 | else: 125 | ffmpegcommand+=["-b:v",str(br)] 126 | 127 | if 'No audio' in options.get('audioChannels','') or passPhase==1: 128 | ffmpegcommand+=["-an"] 129 | elif 'Stereo' in options.get('audioChannels',''): 130 | ffmpegcommand+=["-ac","2"] 131 | ffmpegcommand+=["-ar",'48k'] 132 | ffmpegcommand+=["-b:a",str(audoBitrate)] 133 | elif 'Mono' in options.get('audioChannels',''): 134 | ffmpegcommand+=["-ac","1"] 135 | ffmpegcommand+=["-ar",'48k'] 136 | ffmpegcommand+=["-b:a",str(audoBitrate)] 137 | elif 'Copy' in options.get('audioChannels',''): 138 | ffmpegcommand+=["-c:a","copy"] 139 | else: 140 | ffmpegcommand+=["-an"] 141 | 142 | ffmpegcommand+=["-sn"] 143 | 144 | if passPhase==1: 145 | ffmpegcommand += ['-f', 'null', os.devnull] 146 | else: 147 | ffmpegcommand += [tempVideoFilePath] 148 | 149 | logging.debug("Ffmpeg command: {}".format(' '.join(ffmpegcommand))) 150 | proc = sp.Popen(ffmpegcommand,stderr=sp.PIPE,stdin=sp.DEVNULL,stdout=sp.DEVNULL) 151 | encoderStatusCallback(None,None, lastEncodedBR=br, lastEncodedSize=None, lastBuff=bufsize, lastWR=widthReduction) 152 | psnr, returnCode = logffmpegEncodeProgress(proc,'Pass {} {} {}'.format(passNumber,passReason,tempVideoFilePath),totalEncodedSeconds,totalExpectedEncodedSeconds,encoderStatusCallback,passNumber=passPhase,requestId=requestId,tempVideoPath=tempVideoFilePath,options=options) 153 | if isRquestCancelled(requestId): 154 | return 0, psnr, returnCode 155 | if passPhase==1: 156 | return 0, psnr, returnCode 157 | else: 158 | finalSize = os.stat(tempVideoFilePath).st_size 159 | encoderStatusCallback(None,None,lastEncodedSize=finalSize) 160 | return finalSize, psnr, returnCode 161 | 162 | encoderStatusCallback('Encoding final '+videoFileName,(totalEncodedSeconds)/totalExpectedEncodedSeconds) 163 | 164 | encoderFunction.supportsCRQMode=False 165 | optimiser = encodeTargetingSize_linear 166 | if 'Nelder-Mead' in options.get('optimizer'): 167 | optimiser = encodeTargetingSize_nelder_mead 168 | 169 | finalFilenameConfirmed = optimiser(encoderFunction=encoderFunction, 170 | tempFilename=tempVideoFilePath, 171 | outputFilename=videoFilePath, 172 | initialDependentValue=initialBr, 173 | twoPassMode=True, 174 | allowEarlyExitWhenUndersize=globalOptions.get('allowEarlyExitIfUndersized',True), 175 | sizeLimitMin=sizeLimitMin, 176 | sizeLimitMax=sizeLimitMax, 177 | maxAttempts=globalOptions.get('maxEncodeAttempts',6), 178 | dependentValueMaximum=options.get('maximumBitrate',0), 179 | requestId=requestId, 180 | optimiserName=options.get('optimizer'), 181 | options=options, 182 | globalOptions=globalOptions) 183 | 184 | encoderStatusCallback('Encoding final '+videoFileName,(totalEncodedSeconds)/totalExpectedEncodedSeconds ) 185 | 186 | encoderStatusCallback('Encoding complete '+videoFilePath,1,finalFilename=finalFilenameConfirmed) -------------------------------------------------------------------------------- /src/encoders/mp4H265NvencEncoder.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import logging 4 | import subprocess as sp 5 | 6 | from ..encodingUtils import getFreeNameForFileAndLog 7 | from ..encodingUtils import logffmpegEncodeProgress 8 | from ..encodingUtils import isRquestCancelled 9 | 10 | from ..optimisers.nelderMead import encodeTargetingSize as encodeTargetingSize_nelder_mead 11 | from ..optimisers.linear import encodeTargetingSize as encodeTargetingSize_linear 12 | 13 | def encoder(inputsList, outputPathName,filenamePrefix, filtercommand, options, totalEncodedSeconds, totalExpectedEncodedSeconds, statusCallback,requestId=None,encodeStageFilter='null',globalOptions={},packageglobalStatusCallback=print,startEndTimestamps=None): 14 | 15 | audoBitrate = 8 16 | try: 17 | audoBitrate = int(float(options.get('audioRate','8'))) 18 | except Exception as e: 19 | print(e) 20 | 21 | audoBitrate = int(audoBitrate)*1024 22 | 23 | audio_mp = 8 24 | video_mp = 1024*1024 25 | initialBr = globalOptions.get('initialBr',16777216) 26 | dur = totalExpectedEncodedSeconds-totalEncodedSeconds 27 | 28 | if options.get('maximumSize') == 0.0: 29 | sizeLimitMax = float('inf') 30 | sizeLimitMin = float('-inf') 31 | initialBr = globalOptions.get('initialBr',16777216) 32 | else: 33 | sizeLimitMax = options.get('maximumSize')*1024*1024 34 | sizeLimitMin = sizeLimitMax*(1.0-globalOptions.get('allowableTargetSizeUnderrun',0.25)) 35 | targetSize_guide = (sizeLimitMin+sizeLimitMax)/2 36 | initialBr = ( ((targetSize_guide)/dur) - ((audoBitrate / 1024 / audio_mp)/dur) )*8 37 | 38 | videoFileName,logFilePath,filterFilePath,tempVideoFilePath,videoFilePath = getFreeNameForFileAndLog(filenamePrefix, 'mp4', requestId) 39 | 40 | def encoderStatusCallback(text,percentage,**kwargs): 41 | statusCallback(text,percentage,**kwargs) 42 | packageglobalStatusCallback(text,percentage) 43 | 44 | def encoderFunction(br,passNumber,passReason,passPhase=0,requestId=None,widthReduction=0.0,bufsize=None, cqMode=False): 45 | 46 | ffmpegcommand=[] 47 | ffmpegcommand+=['ffmpeg' ,'-y'] 48 | 49 | if globalOptions.get('passCudaFlags',False): 50 | ffmpegcommand+=['-hwaccel', 'cuda'] 51 | 52 | s,e = None,None 53 | if startEndTimestamps is not None: 54 | s,e = startEndTimestamps 55 | ffmpegcommand+=['-ss', str(s)] 56 | ffmpegcommand+=inputsList 57 | ffmpegcommand+=['-to', str(e-s)] 58 | else: 59 | ffmpegcommand+=inputsList 60 | 61 | if widthReduction>0.0: 62 | encodefiltercommand = filtercommand+',[outv]scale=iw*(1-{widthReduction}):ih*(1-{widthReduction}):flags=bicubic[outvfinal]'.format(widthReduction=widthReduction) 63 | else: 64 | encodefiltercommand = filtercommand+',[outv]null[outvfinal]'.format(widthReduction=widthReduction) 65 | 66 | if options.get('audioChannels') == 'No audio': 67 | encodefiltercommand+=',[outa]anullsink' 68 | 69 | with open(filterFilePath,'wb') as filterFile: 70 | filterFile.write(encodefiltercommand.encode('utf8')) 71 | 72 | if options.get('audioChannels') == 'No audio': 73 | ffmpegcommand+=['-filter_complex_script',filterFilePath] 74 | ffmpegcommand+=['-map','[outvfinal]'] 75 | elif 'Copy' in options.get('audioChannels',''): 76 | ffmpegcommand+=['-filter_complex_script',filterFilePath] 77 | ffmpegcommand+=['-map','[outvfinal]','-map','a:0'] 78 | else: 79 | ffmpegcommand+=['-filter_complex_script',filterFilePath] 80 | ffmpegcommand+=['-map','[outvfinal]','-map','[outa]'] 81 | 82 | if passPhase==1: 83 | ffmpegcommand+=['-pass', '1', '-passlogfile', logFilePath ] 84 | elif passPhase==2: 85 | ffmpegcommand+=['-pass', '2', '-passlogfile', logFilePath ] 86 | 87 | if bufsize is None: 88 | bufsize = 3000000 89 | 90 | threadCount = globalOptions.get('encoderStageThreads',4) 91 | if startEndTimestamps is None: 92 | ffmpegcommand+=["-shortest" 93 | ,"-copyts" 94 | ,"-start_at_zero"] 95 | 96 | 97 | ffmpegcommand+=["-c:v","hevc_nvenc" 98 | ,"-stats" 99 | ,"-profile", "main" 100 | ,"-pix_fmt","yuv420p" 101 | ,"-bufsize", str(bufsize) 102 | ,"-threads", str(threadCount) 103 | ,"-tune", globalOptions.get('mp4NvencTuneParam','hq') 104 | ,"-preset", globalOptions.get('mp4NvencPresetParam','hq') 105 | ,"-crf" ,'17' 106 | ,'-flags','+psnr' 107 | ,"-vsync","vfr" 108 | ,"-movflags","+faststart"] 109 | 110 | if sizeLimitMax == 0.0: 111 | ffmpegcommand+=["-b:v","0","-qmin","0","-qmax","10"] 112 | else: 113 | ffmpegcommand+= ["-b:v", str(br), "-maxrate", str(br)] 114 | 115 | if 'No audio' in options.get('audioChannels','') or passPhase==1: 116 | ffmpegcommand+=["-an"] 117 | elif 'Stereo' in options.get('audioChannels',''): 118 | ffmpegcommand+=["-ac","2"] 119 | ffmpegcommand+=["-ar",str(44100)] 120 | ffmpegcommand+=["-b:a",str(audoBitrate)] 121 | elif 'Mono' in options.get('audioChannels',''): 122 | ffmpegcommand+=["-ac","1"] 123 | ffmpegcommand+=["-ar",str(44100)] 124 | ffmpegcommand+=["-b:a",str(audoBitrate)] 125 | elif 'Copy' in options.get('audioChannels',''): 126 | ffmpegcommand+=["-c:a","copy"] 127 | else: 128 | ffmpegcommand+=["-an"] 129 | 130 | if passPhase==1: 131 | ffmpegcommand += ["-sn",'-f', 'null', os.devnull] 132 | else: 133 | ffmpegcommand += ["-sn",tempVideoFilePath] 134 | 135 | logging.debug("Ffmpeg command: {}".format(' '.join(ffmpegcommand))) 136 | print("Ffmpeg command: {}".format(' '.join(ffmpegcommand))) 137 | 138 | encoderStatusCallback('Encoding final '+videoFileName,(totalEncodedSeconds)/totalExpectedEncodedSeconds) 139 | proc = sp.Popen(ffmpegcommand,stderr=sp.PIPE,stdin=sp.DEVNULL,stdout=sp.DEVNULL) 140 | 141 | encoderStatusCallback(None,None, lastEncodedBR=br, lastEncodedSize=None, lastBuff=bufsize, lastWR=widthReduction) 142 | 143 | psnr, returnCode = logffmpegEncodeProgress(proc,'Pass {} {} {}'.format(passNumber,passReason,videoFileName),totalEncodedSeconds,totalExpectedEncodedSeconds,encoderStatusCallback,passNumber=passPhase,requestId=requestId,tempVideoPath=tempVideoFilePath,options=options) 144 | if isRquestCancelled(requestId): 145 | return 0, psnr, returnCode 146 | if passPhase==1: 147 | return 0, psnr, returnCode 148 | else: 149 | finalSize = os.stat(tempVideoFilePath).st_size 150 | encoderStatusCallback(None,None,lastEncodedSize=finalSize) 151 | return finalSize, psnr, returnCode 152 | 153 | encoderFunction.supportsCRQMode=False 154 | optimiser = encodeTargetingSize_linear 155 | if 'Nelder-Mead' in options.get('optimizer'): 156 | optimiser = encodeTargetingSize_nelder_mead 157 | 158 | 159 | finalFilenameConfirmed = optimiser(encoderFunction=encoderFunction, 160 | tempFilename=tempVideoFilePath, 161 | outputFilename=videoFilePath, 162 | initialDependentValue=initialBr, 163 | allowEarlyExitWhenUndersize=globalOptions.get('allowEarlyExitIfUndersized',True), 164 | twoPassMode=True, 165 | sizeLimitMin=sizeLimitMin, 166 | sizeLimitMax=sizeLimitMax, 167 | maxAttempts=globalOptions.get('maxEncodeAttempts',6), 168 | dependentValueMaximum=options.get('maximumBitrate',0), 169 | requestId=requestId, 170 | optimiserName=options.get('optimizer'), 171 | options=options, 172 | globalOptions=globalOptions) 173 | 174 | encoderStatusCallback('Encoding final '+videoFileName,(totalEncodedSeconds)/totalExpectedEncodedSeconds ) 175 | encoderStatusCallback('Encoding complete '+videoFilePath,1,finalFilename=finalFilenameConfirmed) -------------------------------------------------------------------------------- /src/encoders/mp4x264Encoder.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import logging 4 | import subprocess as sp 5 | 6 | from ..encodingUtils import getFreeNameForFileAndLog 7 | from ..encodingUtils import logffmpegEncodeProgress 8 | from ..encodingUtils import isRquestCancelled 9 | 10 | from ..optimisers.nelderMead import encodeTargetingSize as encodeTargetingSize_nelder_mead 11 | from ..optimisers.linear import encodeTargetingSize as encodeTargetingSize_linear 12 | 13 | def encoder(inputsList, outputPathName,filenamePrefix, filtercommand, options, totalEncodedSeconds, totalExpectedEncodedSeconds, statusCallback,requestId=None,encodeStageFilter='null',globalOptions={},packageglobalStatusCallback=print,startEndTimestamps=None): 14 | 15 | audoBitrate = 8 16 | try: 17 | audoBitrate = int(float(options.get('audioRate','8'))) 18 | except Exception as e: 19 | print(e) 20 | 21 | audoBitrate = int(audoBitrate)*1024 22 | 23 | audio_mp = 8 24 | video_mp = 1024*1024 25 | initialBr = globalOptions.get('initialBr',16777216) 26 | dur = totalExpectedEncodedSeconds-totalEncodedSeconds 27 | 28 | if options.get('maximumSize') == 0.0: 29 | sizeLimitMax = float('inf') 30 | sizeLimitMin = float('-inf') 31 | initialBr = globalOptions.get('initialBr',16777216) 32 | else: 33 | sizeLimitMax = options.get('maximumSize')*1024*1024 34 | sizeLimitMin = sizeLimitMax*(1.0-globalOptions.get('allowableTargetSizeUnderrun',0.25)) 35 | targetSize_guide = (sizeLimitMin+sizeLimitMax)/2 36 | initialBr = ( ((targetSize_guide)/dur) - ((audoBitrate / 1024 / audio_mp)/dur) )*8 37 | 38 | videoFileName,logFilePath,filterFilePath,tempVideoFilePath,videoFilePath = getFreeNameForFileAndLog(filenamePrefix, 'mp4', requestId) 39 | 40 | def encoderStatusCallback(text,percentage,**kwargs): 41 | statusCallback(text,percentage,**kwargs) 42 | packageglobalStatusCallback(text,percentage) 43 | 44 | def encoderFunction(br,passNumber,passReason,passPhase=0,requestId=None,widthReduction=0.0,bufsize=None, cqMode=False): 45 | 46 | ffmpegcommand=[] 47 | ffmpegcommand+=['ffmpeg' ,'-y'] 48 | 49 | s,e = None,None 50 | if startEndTimestamps is not None: 51 | s,e = startEndTimestamps 52 | ffmpegcommand+=['-ss', str(s)] 53 | ffmpegcommand+=inputsList 54 | ffmpegcommand+=['-to', str(e-s)] 55 | else: 56 | ffmpegcommand+=inputsList 57 | 58 | if widthReduction>0.0: 59 | encodefiltercommand = filtercommand+',{encodeStageFilter},[outv]scale=iw*(1-{widthReduction}):ih*(1-{widthReduction}):flags=bicubic[outvfinal]'.format(widthReduction=widthReduction,encodeStageFilter=encodeStageFilter) 60 | else: 61 | encodefiltercommand = filtercommand+',[outv]null,{encodeStageFilter}[outvfinal]'.format(encodeStageFilter=encodeStageFilter) 62 | 63 | if options.get('audioChannels') == 'No audio': 64 | encodefiltercommand+=',[outa]anullsink' 65 | 66 | with open(filterFilePath,'wb') as filterFile: 67 | filterFile.write(encodefiltercommand.encode('utf8')) 68 | 69 | if options.get('audioChannels') == 'No audio': 70 | ffmpegcommand+=['-filter_complex_script',filterFilePath] 71 | ffmpegcommand+=['-map','[outvfinal]'] 72 | elif 'Copy' in options.get('audioChannels',''): 73 | ffmpegcommand+=['-filter_complex_script',filterFilePath] 74 | ffmpegcommand+=['-map','[outvfinal]','-map','a:0'] 75 | else: 76 | ffmpegcommand+=['-filter_complex_script',filterFilePath] 77 | ffmpegcommand+=['-map','[outvfinal]','-map','[outa]'] 78 | 79 | if passPhase==1: 80 | ffmpegcommand+=['-pass', '1', '-passlogfile', logFilePath ] 81 | elif passPhase==2: 82 | ffmpegcommand+=['-pass', '2', '-passlogfile', logFilePath ] 83 | 84 | if bufsize is None: 85 | bufsize = 3000000 86 | 87 | threadCount = globalOptions.get('encoderStageThreads',4) 88 | 89 | if startEndTimestamps is None: 90 | ffmpegcommand+=["-shortest" 91 | ,"-copyts" 92 | ,"-start_at_zero"] 93 | 94 | 95 | ffmpegcommand+=["-c:v","libx264" 96 | ,"-stats" 97 | ,"-max_muxing_queue_size", "9999" 98 | ,"-pix_fmt","yuv420p" 99 | ,"-bufsize", str(bufsize) 100 | ,"-threads", str(threadCount) 101 | ,"-crf" ,'17' 102 | ,"-preset", globalOptions.get('mp4Libx264TuneParam',"slower") 103 | ,"-tune", "film" 104 | ,'-flags','+psnr' 105 | ,"-vsync","vfr" 106 | ,"-movflags","+faststart"] 107 | 108 | if sizeLimitMax == 0.0: 109 | ffmpegcommand+=["-b:v","0","-qmin","0","-qmax","10"] 110 | else: 111 | ffmpegcommand+= ["-b:v", str(br), "-maxrate", str(br)] 112 | 113 | if 'No audio' in options.get('audioChannels','') or passPhase==1: 114 | ffmpegcommand+=["-an"] 115 | elif 'Stereo' in options.get('audioChannels',''): 116 | ffmpegcommand+=["-ac","2"] 117 | ffmpegcommand+=["-ar",str(44100)] 118 | ffmpegcommand+=["-b:a",str(audoBitrate)] 119 | elif 'Mono' in options.get('audioChannels',''): 120 | ffmpegcommand+=["-ac","1"] 121 | ffmpegcommand+=["-ar",str(44100)] 122 | ffmpegcommand+=["-b:a",str(audoBitrate)] 123 | elif 'Copy' in options.get('audioChannels',''): 124 | ffmpegcommand+=["-c:a","copy"] 125 | else: 126 | ffmpegcommand+=["-an"] 127 | 128 | if passPhase==1: 129 | ffmpegcommand += ["-sn",'-f', 'null', os.devnull] 130 | else: 131 | ffmpegcommand += ["-sn",tempVideoFilePath] 132 | 133 | logging.debug("Ffmpeg command: {}".format(' '.join(ffmpegcommand))) 134 | print("Ffmpeg command: {}".format(' '.join(ffmpegcommand))) 135 | 136 | encoderStatusCallback('Encoding final '+videoFileName,(totalEncodedSeconds)/totalExpectedEncodedSeconds) 137 | proc = sp.Popen(ffmpegcommand,stderr=sp.PIPE,stdin=sp.DEVNULL,stdout=sp.DEVNULL) 138 | 139 | encoderStatusCallback(None,None, lastEncodedBR=br, lastEncodedSize=None, lastBuff=bufsize, lastWR=widthReduction) 140 | 141 | psnr, returnCode = logffmpegEncodeProgress(proc,'Pass {} {} {}'.format(passNumber,passReason,videoFileName),totalEncodedSeconds,totalExpectedEncodedSeconds,encoderStatusCallback,passNumber=passPhase,requestId=requestId,options=options) 142 | if isRquestCancelled(requestId): 143 | return 0, psnr, returnCode 144 | if passPhase==1: 145 | return 0, psnr, returnCode 146 | else: 147 | finalSize = os.stat(tempVideoFilePath).st_size 148 | encoderStatusCallback(None,None,lastEncodedSize=finalSize) 149 | return finalSize, psnr, returnCode 150 | 151 | encoderFunction.supportsCRQMode=False 152 | optimiser = encodeTargetingSize_linear 153 | if 'Nelder-Mead' in options.get('optimizer'): 154 | optimiser = encodeTargetingSize_nelder_mead 155 | 156 | 157 | finalFilenameConfirmed = optimiser(encoderFunction=encoderFunction, 158 | tempFilename=tempVideoFilePath, 159 | outputFilename=videoFilePath, 160 | initialDependentValue=initialBr, 161 | twoPassMode=True, 162 | sizeLimitMin=sizeLimitMin, 163 | sizeLimitMax=sizeLimitMax, 164 | allowEarlyExitWhenUndersize=globalOptions.get('allowEarlyExitIfUndersized',True), 165 | maxAttempts=globalOptions.get('maxEncodeAttempts',6), 166 | dependentValueMaximum=options.get('maximumBitrate',0), 167 | requestId=requestId, 168 | optimiserName=options.get('optimizer'), 169 | options=options, 170 | globalOptions=globalOptions) 171 | 172 | encoderStatusCallback('Encoding final '+videoFileName,(totalEncodedSeconds)/totalExpectedEncodedSeconds ) 173 | encoderStatusCallback('Encoding complete '+videoFilePath,1,finalFilename=finalFilenameConfirmed) -------------------------------------------------------------------------------- /src/encoders/mp4x264NvencEncoder.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import logging 4 | import subprocess as sp 5 | 6 | from ..encodingUtils import getFreeNameForFileAndLog 7 | from ..encodingUtils import logffmpegEncodeProgress 8 | from ..encodingUtils import isRquestCancelled 9 | 10 | from ..optimisers.nelderMead import encodeTargetingSize as encodeTargetingSize_nelder_mead 11 | from ..optimisers.linear import encodeTargetingSize as encodeTargetingSize_linear 12 | 13 | def encoder(inputsList, outputPathName,filenamePrefix, filtercommand, options, totalEncodedSeconds, totalExpectedEncodedSeconds, statusCallback,requestId=None,encodeStageFilter='null',globalOptions={},packageglobalStatusCallback=print,startEndTimestamps=None): 14 | 15 | audoBitrate = 8 16 | try: 17 | audoBitrate = int(float(options.get('audioRate','8'))) 18 | except Exception as e: 19 | print(e) 20 | 21 | audoBitrate = int(audoBitrate)*1024 22 | 23 | audio_mp = 8 24 | video_mp = 1024*1024 25 | initialBr = globalOptions.get('initialBr',16777216) 26 | dur = totalExpectedEncodedSeconds-totalEncodedSeconds 27 | 28 | if options.get('maximumSize') == 0.0: 29 | sizeLimitMax = float('inf') 30 | sizeLimitMin = float('-inf') 31 | initialBr = globalOptions.get('initialBr',16777216) 32 | else: 33 | sizeLimitMax = options.get('maximumSize')*1024*1024 34 | sizeLimitMin = sizeLimitMax*(1.0-globalOptions.get('allowableTargetSizeUnderrun',0.25)) 35 | targetSize_guide = (sizeLimitMin+sizeLimitMax)/2 36 | initialBr = ( ((targetSize_guide)/dur) - ((audoBitrate / 1024 / audio_mp)/dur) )*8 37 | 38 | videoFileName,logFilePath,filterFilePath,tempVideoFilePath,videoFilePath = getFreeNameForFileAndLog(filenamePrefix, 'mp4', requestId) 39 | 40 | def encoderStatusCallback(text,percentage,**kwargs): 41 | statusCallback(text,percentage,**kwargs) 42 | packageglobalStatusCallback(text,percentage) 43 | 44 | def encoderFunction(br,passNumber,passReason,passPhase=0,requestId=None,widthReduction=0.0,bufsize=None, cqMode=False): 45 | 46 | ffmpegcommand=[] 47 | ffmpegcommand+=['ffmpeg' ,'-y'] 48 | 49 | 50 | 51 | 52 | 53 | if globalOptions.get('passCudaFlags',False): 54 | ffmpegcommand+=['-hwaccel', 'cuda'] 55 | 56 | s,e = None,None 57 | if startEndTimestamps is not None: 58 | s,e = startEndTimestamps 59 | ffmpegcommand+=['-ss', str(s)] 60 | ffmpegcommand+=inputsList 61 | ffmpegcommand+=['-to', str(e-s)] 62 | else: 63 | ffmpegcommand+=inputsList 64 | 65 | if widthReduction>0.0: 66 | encodefiltercommand = filtercommand+',{encodeStageFilter},[outv]scale=iw*(1-{widthReduction}):ih*(1-{widthReduction}):flags=bicubic[outvfinal]'.format(widthReduction=widthReduction,encodeStageFilter=encodeStageFilter) 67 | else: 68 | encodefiltercommand = filtercommand+',[outv]null,{encodeStageFilter}[outvfinal]'.format(encodeStageFilter=encodeStageFilter) 69 | 70 | 71 | if options.get('audioChannels') == 'No audio': 72 | encodefiltercommand+=',[outa]anullsink' 73 | 74 | with open(filterFilePath,'wb') as filterFile: 75 | filterFile.write(encodefiltercommand.encode('utf8')) 76 | 77 | if options.get('audioChannels') == 'No audio': 78 | ffmpegcommand+=['-filter_complex_script',filterFilePath] 79 | ffmpegcommand+=['-map','[outvfinal]'] 80 | elif 'Copy' in options.get('audioChannels',''): 81 | ffmpegcommand+=['-filter_complex_script',filterFilePath] 82 | ffmpegcommand+=['-map','[outvfinal]','-map','a:0'] 83 | else: 84 | ffmpegcommand+=['-filter_complex_script',filterFilePath] 85 | ffmpegcommand+=['-map','[outvfinal]','-map','[outa]'] 86 | 87 | if passPhase==1: 88 | ffmpegcommand+=['-pass', '1', '-passlogfile', logFilePath ] 89 | elif passPhase==2: 90 | ffmpegcommand+=['-pass', '2', '-passlogfile', logFilePath ] 91 | 92 | if bufsize is None: 93 | bufsize = 3000000 94 | 95 | threadCount = globalOptions.get('encoderStageThreads',4) 96 | 97 | if startEndTimestamps is None: 98 | ffmpegcommand+=["-shortest" 99 | ,"-copyts" 100 | ,"-start_at_zero"] 101 | 102 | 103 | ffmpegcommand+=["-c:v","h264_nvenc" 104 | ,"-stats" 105 | ,"-max_muxing_queue_size", "9999" 106 | ,"-preset", "slow" 107 | ,"-pix_fmt","yuv420p" 108 | ,"-bufsize", str(bufsize) 109 | ,"-threads", str(threadCount) 110 | ,"-tune", globalOptions.get('mp4NvencTuneParam','hq') 111 | ,"-preset", globalOptions.get('mp4NvencPresetParam','hq') 112 | ,"-b_ref_mode", "middle" 113 | ,"-temporal-aq", "1" 114 | ,"-rc-lookahead", "20" 115 | ,"-crf" ,'17' 116 | ,'-flags','+psnr' 117 | ,"-vsync","vfr" 118 | ,"-movflags","+faststart"] 119 | 120 | if sizeLimitMax == 0.0: 121 | ffmpegcommand+=["-b:v","0","-qmin","0","-qmax","10"] 122 | else: 123 | ffmpegcommand+= ["-b:v", str(br), "-maxrate", str(br)] 124 | 125 | if 'No audio' in options.get('audioChannels','') or passPhase==1: 126 | ffmpegcommand+=["-an"] 127 | elif 'Stereo' in options.get('audioChannels',''): 128 | ffmpegcommand+=["-ac","2"] 129 | ffmpegcommand+=["-ar",str(44100)] 130 | ffmpegcommand+=["-b:a",str(audoBitrate)] 131 | elif 'Mono' in options.get('audioChannels',''): 132 | ffmpegcommand+=["-ac","1"] 133 | ffmpegcommand+=["-ar",str(44100)] 134 | ffmpegcommand+=["-b:a",str(audoBitrate)] 135 | elif 'Copy' in options.get('audioChannels',''): 136 | ffmpegcommand+=["-c:a","copy"] 137 | else: 138 | ffmpegcommand+=["-an"] 139 | 140 | if passPhase==1: 141 | ffmpegcommand += ["-sn",'-f', 'null', os.devnull] 142 | else: 143 | ffmpegcommand += ["-sn",tempVideoFilePath] 144 | 145 | logging.debug("Ffmpeg command: {}".format(' '.join(ffmpegcommand))) 146 | print("Ffmpeg command: {}".format(' '.join(ffmpegcommand))) 147 | 148 | encoderStatusCallback('Encoding final '+videoFileName,(totalEncodedSeconds)/totalExpectedEncodedSeconds,pix_fmt='') 149 | proc = sp.Popen(ffmpegcommand,stderr=sp.PIPE,stdin=sp.DEVNULL,stdout=sp.DEVNULL) 150 | 151 | encoderStatusCallback(None,None, lastEncodedBR=br, lastEncodedSize=None, lastBuff=bufsize, lastWR=widthReduction) 152 | 153 | psnr, returnCode = logffmpegEncodeProgress(proc,'Pass {} {} {}'.format(passNumber,passReason,videoFileName),totalEncodedSeconds,totalExpectedEncodedSeconds,encoderStatusCallback,passNumber=passPhase,requestId=requestId,options=options) 154 | if isRquestCancelled(requestId): 155 | return 0, psnr, returnCode 156 | if passPhase==1: 157 | return 0, psnr, returnCode 158 | else: 159 | finalSize = os.stat(tempVideoFilePath).st_size 160 | encoderStatusCallback(None,None,lastEncodedSize=finalSize) 161 | return finalSize, psnr, returnCode 162 | 163 | encoderFunction.supportsCRQMode=False 164 | optimiser = encodeTargetingSize_linear 165 | if 'Nelder-Mead' in options.get('optimizer'): 166 | optimiser = encodeTargetingSize_nelder_mead 167 | 168 | 169 | finalFilenameConfirmed = optimiser(encoderFunction=encoderFunction, 170 | tempFilename=tempVideoFilePath, 171 | outputFilename=videoFilePath, 172 | initialDependentValue=initialBr, 173 | allowEarlyExitWhenUndersize=globalOptions.get('allowEarlyExitIfUndersized',True), 174 | twoPassMode=True, 175 | sizeLimitMin=sizeLimitMin, 176 | sizeLimitMax=sizeLimitMax, 177 | maxAttempts=globalOptions.get('maxEncodeAttempts',6), 178 | dependentValueMaximum=options.get('maximumBitrate',0), 179 | requestId=requestId, 180 | optimiserName=options.get('optimizer'), 181 | options=options, 182 | globalOptions=globalOptions) 183 | 184 | encoderStatusCallback('Encoding final '+videoFileName,(totalEncodedSeconds)/totalExpectedEncodedSeconds ) 185 | encoderStatusCallback('Encoding complete '+videoFilePath,1,finalFilename=finalFilenameConfirmed) -------------------------------------------------------------------------------- /src/encoders/specVideoEncoder.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import logging 4 | import subprocess as sp 5 | import datetime 6 | import time 7 | import json 8 | 9 | from ..encodingUtils import getFreeNameForFileAndLog 10 | from ..encodingUtils import logffmpegEncodeProgress 11 | from ..encodingUtils import isRquestCancelled 12 | 13 | from ..webmGeneratorUi import RELEASE_NUMVER 14 | 15 | from ..optimisers.nelderMead import encodeTargetingSize as encodeTargetingSize_nelder_mead 16 | from ..optimisers.linear import encodeTargetingSize as encodeTargetingSize_linear 17 | 18 | class SpecVideoEncoder: 19 | 20 | def __init__(self,specFilename): 21 | self.specFilename = specFilename 22 | json.load(open(self.specFilename)) 23 | 24 | def getExtraEncoderParams(self): 25 | extraParams = [] 26 | spec = json.load(open(self.specFilename)) 27 | for commandBlock in spec.get('commandBlocks', []): 28 | if 'selection' in commandBlock: 29 | selection = commandBlock.get('selection', {}) 30 | paramsSpec = {'name': selection.get('name'), 'label':selection.get('label',selection.get('name'))} 31 | if 'options' in selection: 32 | paramsSpec['type'] = 'choice' 33 | paramsSpec['default'] = selection.get('default', 'None') 34 | for option in selection.get('options', []): 35 | paramsSpec.setdefault('options', []).append(option.get('name')) 36 | elif 'type' in selection: 37 | paramsSpec['default'] = selection.get('default', 0) 38 | paramsSpec['type'] = selection.get('type', 'int') 39 | 40 | extraParams.append(paramsSpec) 41 | return extraParams 42 | 43 | def validate(self): 44 | spec = json.load(open(self.specFilename)) 45 | return True 46 | 47 | def getDisplayName(self): 48 | spec = json.load(open(self.specFilename)) 49 | return spec['extension']+':'+spec['name'] 50 | 51 | def __call__(self, inputsList, outputPathName,filenamePrefix, filtercommand, options, totalEncodedSeconds, totalExpectedEncodedSeconds, statusCallback,requestId=None,encodeStageFilter='null',globalOptions={},packageglobalStatusCallback=print,startEndTimestamps=None): 52 | 53 | spec = json.load(open(self.specFilename)) 54 | specOptions = globalOptions.copy() 55 | specOptions.update(options.copy()) 56 | 57 | audoBitrate = 8 58 | try: 59 | audoBitrate = int(float(options.get('audioRate','8'))) 60 | except Exception as e: 61 | print(e) 62 | 63 | audoBitrate = int(audoBitrate)*1024 64 | 65 | audio_mp = 8 66 | video_mp = 1024*1024 67 | initialBr = globalOptions.get('initialBr',16777216) 68 | dur = totalExpectedEncodedSeconds-totalEncodedSeconds 69 | 70 | if options.get('maximumSize') == 0.0: 71 | sizeLimitMax = float('inf') 72 | sizeLimitMin = float('-inf') 73 | initialBr = globalOptions.get('initialBr',16777216) 74 | else: 75 | sizeLimitMax = options.get('maximumSize')*1024*1024 76 | sizeLimitMin = sizeLimitMax*(1.0-globalOptions.get('allowableTargetSizeUnderrun',0.25)) 77 | targetSize_guide = (sizeLimitMin+sizeLimitMax)/2 78 | initialBr = ( ((targetSize_guide)/dur) - ((audoBitrate / 1024 / audio_mp)/dur) )*8 79 | 80 | print('allowableTargetSizeUnderrun',globalOptions.get('allowableTargetSizeUnderrun')) 81 | print('sizeLimitMax',sizeLimitMax) 82 | print('sizeLimitMin',sizeLimitMin) 83 | 84 | videoFileName,logFilePath,filterFilePath,tempVideoFilePath,videoFilePath = getFreeNameForFileAndLog(filenamePrefix, spec['extension'], requestId) 85 | 86 | specOptions['audoBitrate'] = audoBitrate 87 | specOptions['sizeLimitMax'] = audoBitrate 88 | specOptions['sizeLimitMin'] = audoBitrate 89 | 90 | metadataSuffix = globalOptions.get('titleMetadataSuffix',' WmG') 91 | 92 | specOptions['metadata_title'] = '{}'.format(filenamePrefix.replace('-','-') + metadataSuffix) 93 | 94 | def encoderStatusCallback(text,percentage,**kwargs): 95 | statusCallback(text,percentage,**kwargs) 96 | packageglobalStatusCallback(text,percentage) 97 | 98 | def encoderFunction(br, passNumber, passReason, passPhase=0, requestId=None, widthReduction=0.0, bufsize=None, cqMode=False): 99 | 100 | ffmpegcommand=[] 101 | ffmpegcommand+=['ffmpeg' ,'-y'] 102 | ffmpegcommand+=inputsList 103 | 104 | specOptions['passPhase'] = passPhase 105 | 106 | if widthReduction>0.0: 107 | encodefiltercommand = filtercommand+',[outv]{encodeStageFilter},scale=iw*(1-{widthReduction}):ih*(1-{widthReduction}):flags=bicubic[outvfinal]'.format(encodeStageFilter=encodeStageFilter,widthReduction=widthReduction) 108 | else: 109 | encodefiltercommand = filtercommand+',[outv]{encodeStageFilter},null[outvfinal]'.format(encodeStageFilter=encodeStageFilter) 110 | 111 | if options.get('audioChannels') == 'No audio': 112 | encodefiltercommand+=',[outa]anullsink' 113 | 114 | with open(filterFilePath,'wb') as filterFile: 115 | filterFile.write(encodefiltercommand.encode('utf8')) 116 | 117 | targetWidth = 0 118 | targetHeight = 0 119 | 120 | try: 121 | widthCmd = ['ffmpeg']+inputsList+['-filter_complex_script',filterFilePath,'-frames:v','1','-f','null','-'] 122 | proc = sp.Popen(widthCmd,stdout=sp.PIPE,stderr=sp.PIPE) 123 | outs,errs = proc.communicate() 124 | for errLine in errs.split(b'\n'): 125 | for errElem in [x.strip() for x in errLine.split(b',')]: 126 | if b'x' in errElem: 127 | try: 128 | w,h = errElem.split(b' ')[0].split(b'x') 129 | w=int(w) 130 | h=int(h) 131 | targetWidth = w 132 | targetHeight = h 133 | except: 134 | pass 135 | except Exception as e: 136 | print(e) 137 | 138 | tileColumns = 0 139 | 140 | if not options.get('disableVP9Tiling',False): 141 | if targetWidth >= 960: 142 | tileColumns = 1 143 | if targetWidth >= 1920: 144 | tileColumns = 2 145 | if targetWidth >= 3840: 146 | tileColumns = 3 147 | 148 | specOptions['tileColumns'] = tileColumns 149 | 150 | if options.get('audioChannels') == 'No audio': 151 | ffmpegcommand+=['-filter_complex_script',filterFilePath] 152 | ffmpegcommand+=['-map','[outvfinal]'] 153 | elif 'Copy' in options.get('audioChannels',''): 154 | ffmpegcommand+=['-filter_complex_script',filterFilePath] 155 | ffmpegcommand+=['-map','[outvfinal]','-map','a:0'] 156 | else: 157 | ffmpegcommand+=['-filter_complex_script',filterFilePath] 158 | ffmpegcommand+=['-map','[outvfinal]','-map','[outa]'] 159 | 160 | if passPhase==1: 161 | ffmpegcommand+=['-pass', '1', '-passlogfile', logFilePath ] 162 | elif passPhase==2: 163 | ffmpegcommand+=['-pass', '2', '-passlogfile', logFilePath ] 164 | 165 | if bufsize is None: 166 | bufsize = 3000000 167 | if sizeLimitMax != 0.0 and not cqMode: 168 | bufsize = str(min(2000000000.0,br*2)) 169 | 170 | threadCount = globalOptions.get('encoderStageThreads',4) 171 | metadataSuffix = globalOptions.get('titleMetadataSuffix',' WmG') 172 | 173 | crf = 4 174 | if cqMode: 175 | crf = br 176 | br = 0 177 | 178 | specOptions['crf'] = crf 179 | specOptions['br'] = br 180 | specOptions['bufsize'] = bufsize 181 | 182 | print(specOptions) 183 | 184 | for commandBlock in spec.get('commandBlocks',[]): 185 | conditionPassed = True 186 | 187 | if 'conditions' in commandBlock: 188 | conditions = commandBlock.get('conditions',[]) 189 | for condition in conditions: 190 | if len(condition) == 1: 191 | conditionPassed = conditionPassed & bool(specOptions.get(condition[0], False)) 192 | else: 193 | reference,comparison,value = condition 194 | if comparison.upper() == 'CONTAINS': 195 | conditionPassed = conditionPassed and bool(value.upper() in str(specOptions.get('reference')).upper()) 196 | if comparison.upper() == 'EQUALS': 197 | conditionPassed = conditionPassed and bool(value.upper() == str(specOptions.get('reference')).upper()) 198 | 199 | if conditionPassed: 200 | for cmd in commandBlock.get('cmds',[]): 201 | ffmpegcommand.append(cmd.format(**specOptions)) 202 | else: 203 | for cmd in commandBlock.get('altCmds',[]): 204 | ffmpegcommand.append(cmd.format(**specOptions)) 205 | 206 | elif 'selection' in commandBlock: 207 | selection = commandBlock.get('selection',{}) 208 | 209 | if 'options' in selection: 210 | default_selection = None 211 | matching_selection = None 212 | for option in selection.get('options',[]): 213 | if option.get('name','') == selection.get('default'): 214 | default_selection = option.get('cmds',[]) 215 | if option.get('name','') == specOptions.get('encoder-option-'+selection.get('name','')): 216 | matching_selection = option.get('cmds',[]) 217 | 218 | final_commands = [] 219 | if matching_selection is not None: 220 | final_commands = matching_selection 221 | elif default_selection is not None: 222 | final_commands = default_selection 223 | 224 | for cmd in final_commands: 225 | ffmpegcommand.append(cmd.format(**specOptions)) 226 | elif 'type' in selection: 227 | for cmd in selection.get('cmds',[]): 228 | cmd = cmd.format(value=selection.get('default','')) 229 | ffmpegcommand.append(cmd.format(**specOptions)) 230 | else: 231 | for cmd in commandBlock.get('cmds',[]): 232 | ffmpegcommand.append(cmd.format(**specOptions)) 233 | 234 | if passPhase==1: 235 | ffmpegcommand += ['-f', 'null', os.devnull] 236 | else: 237 | ffmpegcommand += [tempVideoFilePath] 238 | 239 | logging.debug("Ffmpeg command: {}".format(' '.join(ffmpegcommand))) 240 | 241 | print("Ffmpeg command: {}".format(' '.join(ffmpegcommand))) 242 | 243 | proc = sp.Popen(ffmpegcommand,stderr=sp.PIPE,stdin=sp.DEVNULL,stdout=sp.DEVNULL) 244 | 245 | if cqMode: 246 | encoderStatusCallback(None,None, lastEncodedCRF=crf, lastEncodedSize=None, lastBuff=0, lastWR=widthReduction) 247 | else: 248 | encoderStatusCallback(None,None, lastEncodedBR=br, lastEncodedSize=None, lastBuff=bufsize, lastWR=widthReduction) 249 | 250 | 251 | psnr, returnCode = logffmpegEncodeProgress(proc,'Pass {} {} {}'.format(passNumber,passReason,tempVideoFilePath),totalEncodedSeconds,totalExpectedEncodedSeconds,encoderStatusCallback,passNumber=passPhase,requestId=requestId,tempVideoPath=tempVideoFilePath,options=options) 252 | if isRquestCancelled(requestId): 253 | return 0, psnr, returnCode 254 | if passPhase==1: 255 | return 0, psnr, returnCode 256 | else: 257 | finalSize = os.stat(tempVideoFilePath).st_size 258 | encoderStatusCallback(None,None,lastEncodedSize=finalSize) 259 | return finalSize, psnr, returnCode 260 | 261 | encoderFunction.supportsCRQMode=True 262 | encoderStatusCallback('Encoding final '+videoFileName,(totalEncodedSeconds)/totalExpectedEncodedSeconds) 263 | 264 | minimumPSNR = 0.0 265 | try: 266 | minimumPSNR = float(options.get('minimumPSNR',0.0)) 267 | except: 268 | pass 269 | 270 | 271 | optimiser = encodeTargetingSize_linear 272 | if 'Nelder-Mead' in options.get('optimizer'): 273 | optimiser = encodeTargetingSize_nelder_mead 274 | 275 | finalFilenameConfirmed = optimiser(encoderFunction=encoderFunction, 276 | tempFilename=tempVideoFilePath, 277 | outputFilename=videoFilePath, 278 | initialDependentValue=initialBr, 279 | twoPassMode=True, 280 | allowEarlyExitWhenUndersize=globalOptions.get('allowEarlyExitIfUndersized',True), 281 | sizeLimitMin=sizeLimitMin, 282 | sizeLimitMax=sizeLimitMax, 283 | maxAttempts=globalOptions.get('maxEncodeAttempts',6), 284 | dependentValueMaximum=options.get('maximumBitrate',0), 285 | requestId=requestId, 286 | minimumPSNR=minimumPSNR, 287 | optimiserName=options.get('optimizer'), 288 | options=options, 289 | globalOptions=globalOptions) 290 | 291 | encoderStatusCallback('Encoding final '+videoFileName,(totalEncodedSeconds)/totalExpectedEncodedSeconds ) 292 | 293 | encoderStatusCallback('Encoding complete '+videoFilePath,1,finalFilename=finalFilenameConfirmed) -------------------------------------------------------------------------------- /src/encoders/webmvp8Encoder.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import logging 4 | import subprocess as sp 5 | import datetime 6 | 7 | from ..encodingUtils import getFreeNameForFileAndLog 8 | from ..encodingUtils import logffmpegEncodeProgress 9 | from ..encodingUtils import isRquestCancelled 10 | 11 | from ..webmGeneratorUi import RELEASE_NUMVER 12 | 13 | from ..optimisers.nelderMead import encodeTargetingSize as encodeTargetingSize_nelder_mead 14 | from ..optimisers.linear import encodeTargetingSize as encodeTargetingSize_linear 15 | 16 | def encoder(inputsList, outputPathName,filenamePrefix, filtercommand, options, totalEncodedSeconds, totalExpectedEncodedSeconds, statusCallback,requestId=None,encodeStageFilter='null',globalOptions={},packageglobalStatusCallback=print,startEndTimestamps=None): 17 | 18 | audoBitrate = 8 19 | try: 20 | audoBitrate = int(float(options.get('audioRate','8'))) 21 | except Exception as e: 22 | print(e) 23 | 24 | audoBitrate = int(audoBitrate)*1024 25 | 26 | audio_mp = 8 27 | video_mp = 1024*1024 28 | initialBr = globalOptions.get('initialBr',16777216) 29 | dur = totalExpectedEncodedSeconds-totalEncodedSeconds 30 | 31 | if options.get('maximumSize') == 0.0: 32 | sizeLimitMax = float('inf') 33 | sizeLimitMin = float('-inf') 34 | initialBr = globalOptions.get('initialBr',16777216) 35 | else: 36 | sizeLimitMax = options.get('maximumSize')*1024*1024 37 | sizeLimitMin = sizeLimitMax*(1.0-globalOptions.get('allowableTargetSizeUnderrun',0.25)) 38 | targetSize_guide = (sizeLimitMin+sizeLimitMax)/2 39 | initialBr = ( ((targetSize_guide)/dur) - ((audoBitrate / 1024 / audio_mp)/dur) )*8 40 | 41 | videoFileName,logFilePath,filterFilePath,tempVideoFilePath,videoFilePath = getFreeNameForFileAndLog(filenamePrefix, 'webm', requestId) 42 | 43 | def encoderStatusCallback(text,percentage,**kwargs): 44 | statusCallback(text,percentage,**kwargs) 45 | packageglobalStatusCallback(text,percentage) 46 | 47 | def encoderFunction(br, passNumber, passReason, passPhase=0, requestId=None, widthReduction=0.0, bufsize=None, cqMode=False): 48 | 49 | ffmpegcommand=[] 50 | ffmpegcommand+=['ffmpeg' ,'-y'] 51 | 52 | s,e = None,None 53 | if startEndTimestamps is not None: 54 | s,e = startEndTimestamps 55 | ffmpegcommand+=['-ss', str(s)] 56 | ffmpegcommand+=inputsList 57 | ffmpegcommand+=['-to', str(e-s)] 58 | else: 59 | ffmpegcommand+=inputsList 60 | 61 | 62 | if widthReduction>0.0: 63 | encodefiltercommand = filtercommand+',[outv]{encodeStageFilter},scale=iw*(1-{widthReduction}):ih*(1-{widthReduction}):flags=bicubic[outvfinal]'.format(encodeStageFilter=encodeStageFilter,widthReduction=widthReduction) 64 | else: 65 | encodefiltercommand = filtercommand+',[outv]{encodeStageFilter},null[outvfinal]'.format(encodeStageFilter=encodeStageFilter) 66 | 67 | if options.get('audioChannels') == 'No audio': 68 | encodefiltercommand+=',[outa]anullsink' 69 | 70 | with open(filterFilePath,'wb') as filterFile: 71 | filterFile.write(encodefiltercommand.encode('utf8')) 72 | 73 | if options.get('audioChannels') == 'No audio': 74 | ffmpegcommand+=['-filter_complex_script',filterFilePath] 75 | ffmpegcommand+=['-map','[outvfinal]'] 76 | elif 'Copy' in options.get('audioChannels',''): 77 | ffmpegcommand+=['-filter_complex_script',filterFilePath] 78 | ffmpegcommand+=['-map','[outvfinal]','-map','a:0'] 79 | else: 80 | ffmpegcommand+=['-filter_complex_script',filterFilePath] 81 | ffmpegcommand+=['-map','[outvfinal]','-map','[outa]'] 82 | 83 | if passPhase==1: 84 | ffmpegcommand+=['-pass', '1', '-passlogfile', logFilePath ] 85 | elif passPhase==2: 86 | ffmpegcommand+=['-pass', '2', '-passlogfile', logFilePath ] 87 | 88 | crf = 4 89 | if cqMode: 90 | crf = br 91 | br = 0 92 | 93 | if bufsize is None: 94 | bufsize = 3000000 95 | if sizeLimitMax != 0.0 and not cqMode: 96 | bufsize = str(min(2000000000.0,br*2)) 97 | threadCount = globalOptions.get('encoderStageThreads',4) 98 | metadataSuffix = globalOptions.get('titleMetadataSuffix',' WmG') 99 | 100 | audioCodec = ["-c:a","libvorbis"] 101 | if 'Copy' in options.get('audioChannels',''): 102 | audioCodec = [] 103 | 104 | 105 | if startEndTimestamps is None: 106 | ffmpegcommand+=["-shortest", "-slices", "8", "-copyts" 107 | ,"-start_at_zero", "-c:v","libvpx"] + audioCodec + [ 108 | "-stats","-pix_fmt","yuv420p"] 109 | else: 110 | ffmpegcommand+=["-slices", "8" 111 | ,"-c:v","libvpx"] + audioCodec + [ 112 | "-stats","-pix_fmt","yuv420p"] 113 | 114 | if not cqMode: 115 | ffmpegcommand+=["-bufsize", str(bufsize)] 116 | 117 | 118 | 119 | ffmpegcommand+=["-threads", str(threadCount),"-crf" ,str(crf) 120 | ,"-auto-alt-ref", "1", "-lag-in-frames", str(globalOptions.get('vp8lagInFrames',25)) 121 | ,"-deadline","best",'-slices','8','-cpu-used','16','-flags','+psnr','-movflags','+faststart','-f','webm' 122 | ,"-metadata", 'title={}'.format(filenamePrefix + metadataSuffix) 123 | ,"-metadata", 'WritingApp=WebmGenerator {}'.format(RELEASE_NUMVER) 124 | ,"-metadata", 'DateUTC={}'.format(datetime.datetime.utcnow().isoformat() ) 125 | 126 | ] 127 | 128 | qmaxOverride = 50 129 | 130 | try: 131 | temp = options.get('qmaxOverride',-1) 132 | if temp >= 0: 133 | qmaxOverride = temp 134 | except Exception as e: 135 | print(e) 136 | 137 | print(ffmpegcommand) 138 | if sizeLimitMax == 0.0: 139 | ffmpegcommand+=["-b:v","0","-qmin","0","-qmax",str(qmaxOverride)] 140 | else: 141 | ffmpegcommand+=["-b:v",str(br)] 142 | 143 | 144 | bitRateControl = options.get('bitRateControl','Average') 145 | 146 | if sizeLimitMax > 0.0: 147 | if bitRateControl == 'Limit Maximum': 148 | ffmpegcommand+=['-maxrate' ,str(br)] 149 | elif bitRateControl == 'Constant': 150 | ffmpegcommand+=['-minrate', str(br), '-maxrate' ,str(br)] 151 | 152 | 153 | if 'No audio' in options.get('audioChannels','') or passPhase==1: 154 | ffmpegcommand+=["-an"] 155 | elif 'Stereo' in options.get('audioChannels',''): 156 | ffmpegcommand+=["-ac","2"] 157 | ffmpegcommand+=["-ar",str(44100)] 158 | ffmpegcommand+=["-b:a",str(audoBitrate)] 159 | elif 'Mono' in options.get('audioChannels',''): 160 | ffmpegcommand+=["-ac","1"] 161 | ffmpegcommand+=["-ar",str(44100)] 162 | ffmpegcommand+=["-b:a",str(audoBitrate)] 163 | elif 'Copy' in options.get('audioChannels',''): 164 | ffmpegcommand+=["-c:a","copy"] 165 | else: 166 | ffmpegcommand+=["-an"] 167 | 168 | ffmpegcommand+=["-sn"] 169 | 170 | if passPhase==1: 171 | ffmpegcommand += ['-f', 'null', os.devnull] 172 | else: 173 | ffmpegcommand += [tempVideoFilePath] 174 | 175 | logging.debug("Ffmpeg command: {}".format(' '.join(ffmpegcommand))) 176 | proc = sp.Popen(ffmpegcommand,stderr=sp.PIPE,stdin=sp.DEVNULL,stdout=sp.DEVNULL) 177 | 178 | if cqMode: 179 | encoderStatusCallback(None,None, lastEncodedCRF=crf, lastEncodedSize=None, lastBuff=0, lastWR=widthReduction) 180 | else: 181 | encoderStatusCallback(None,None, lastEncodedBR=br, lastEncodedSize=None, lastBuff=bufsize, lastWR=widthReduction) 182 | 183 | psnr, returnCode = logffmpegEncodeProgress(proc,'Pass {} {} {}'.format(passNumber,passReason,tempVideoFilePath),totalEncodedSeconds,totalExpectedEncodedSeconds,encoderStatusCallback,passNumber=passPhase,requestId=requestId,options=options) 184 | if isRquestCancelled(requestId): 185 | return 0, psnr, returnCode 186 | if passPhase==1: 187 | return 0, psnr, returnCode 188 | else: 189 | finalSize = os.stat(tempVideoFilePath).st_size 190 | encoderStatusCallback(None,None,lastEncodedSize=finalSize) 191 | return finalSize, psnr, returnCode 192 | 193 | encoderFunction.supportsCRQMode=True 194 | encoderStatusCallback('Encoding final '+videoFileName,(totalEncodedSeconds)/totalExpectedEncodedSeconds) 195 | 196 | minimumPSNR = 0.0 197 | try: 198 | minimumPSNR = float(options.get('minimumPSNR',0.0)) 199 | except: 200 | pass 201 | 202 | optimiser = encodeTargetingSize_linear 203 | if 'Nelder-Mead' in options.get('optimizer'): 204 | optimiser = encodeTargetingSize_nelder_mead 205 | 206 | finalFilenameConfirmed = optimiser(encoderFunction=encoderFunction, 207 | tempFilename=tempVideoFilePath, 208 | outputFilename=videoFilePath, 209 | initialDependentValue=initialBr, 210 | twoPassMode=True, 211 | sizeLimitMin=sizeLimitMin, 212 | sizeLimitMax=sizeLimitMax, 213 | allowEarlyExitWhenUndersize=globalOptions.get('allowEarlyExitIfUndersized',True), 214 | maxAttempts=globalOptions.get('maxEncodeAttempts',6), 215 | dependentValueMaximum=options.get('maximumBitrate',0), 216 | requestId=requestId, 217 | minimumPSNR=minimumPSNR, 218 | optimiserName=options.get('optimizer'), 219 | options=options, 220 | globalOptions=globalOptions ) 221 | 222 | encoderStatusCallback('Encoding final '+videoFileName,(totalEncodedSeconds)/totalExpectedEncodedSeconds ) 223 | encoderStatusCallback('Encoding complete '+videoFilePath,1,finalFilename=finalFilenameConfirmed) 224 | -------------------------------------------------------------------------------- /src/encoders/webmvp9Encoder.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import logging 4 | import subprocess as sp 5 | import datetime 6 | import time 7 | 8 | from ..encodingUtils import getFreeNameForFileAndLog 9 | from ..encodingUtils import logffmpegEncodeProgress 10 | from ..encodingUtils import isRquestCancelled 11 | 12 | from ..webmGeneratorUi import RELEASE_NUMVER 13 | 14 | from ..optimisers.nelderMead import encodeTargetingSize as encodeTargetingSize_nelder_mead 15 | from ..optimisers.linear import encodeTargetingSize as encodeTargetingSize_linear 16 | 17 | def encoder(inputsList, outputPathName,filenamePrefix, filtercommand, options, totalEncodedSeconds, totalExpectedEncodedSeconds, statusCallback,requestId=None,encodeStageFilter='null',globalOptions={},packageglobalStatusCallback=print,startEndTimestamps=None): 18 | 19 | audoBitrate = 8 20 | try: 21 | audoBitrate = int(float(options.get('audioRate','8'))) 22 | except Exception as e: 23 | print(e) 24 | 25 | audoBitrate = int(audoBitrate)*1024 26 | 27 | audio_mp = 8 28 | video_mp = 1024*1024 29 | initialBr = globalOptions.get('initialBr',16777216) 30 | dur = totalExpectedEncodedSeconds-totalEncodedSeconds 31 | 32 | if options.get('maximumSize') == 0.0: 33 | sizeLimitMax = float('inf') 34 | sizeLimitMin = float('-inf') 35 | initialBr = globalOptions.get('initialBr',16777216) 36 | else: 37 | sizeLimitMax = options.get('maximumSize')*1024*1024 38 | sizeLimitMin = sizeLimitMax*(1.0-globalOptions.get('allowableTargetSizeUnderrun',0.25)) 39 | targetSize_guide = (sizeLimitMin+sizeLimitMax)/2 40 | initialBr = ( ((targetSize_guide)/dur) - ((audoBitrate / 1024 / audio_mp)/dur) )*8 41 | 42 | print('allowableTargetSizeUnderrun',globalOptions.get('allowableTargetSizeUnderrun')) 43 | print('sizeLimitMax',sizeLimitMax) 44 | print('sizeLimitMin',sizeLimitMin) 45 | 46 | videoFileName,logFilePath,filterFilePath,tempVideoFilePath,videoFilePath = getFreeNameForFileAndLog(filenamePrefix, 'webm', requestId) 47 | 48 | def encoderStatusCallback(text,percentage,**kwargs): 49 | statusCallback(text,percentage,**kwargs) 50 | packageglobalStatusCallback(text,percentage) 51 | 52 | def encoderFunction(br, passNumber, passReason, passPhase=0, requestId=None, widthReduction=0.0, bufsize=None, cqMode=False): 53 | 54 | ffmpegcommand=[] 55 | ffmpegcommand+=['ffmpeg' ,'-y'] 56 | 57 | s,e = None,None 58 | if startEndTimestamps is not None: 59 | s,e = startEndTimestamps 60 | ffmpegcommand+=['-ss', str(s)] 61 | ffmpegcommand+=inputsList 62 | ffmpegcommand+=['-to', str(e-s)] 63 | else: 64 | ffmpegcommand+=inputsList 65 | 66 | if widthReduction>0.0: 67 | encodefiltercommand = filtercommand+',[outv]{encodeStageFilter},scale=iw*(1-{widthReduction}):ih*(1-{widthReduction}):flags=bicubic[outvfinal]'.format(encodeStageFilter=encodeStageFilter,widthReduction=widthReduction) 68 | else: 69 | encodefiltercommand = filtercommand+',[outv]{encodeStageFilter},null[outvfinal]'.format(encodeStageFilter=encodeStageFilter) 70 | 71 | if options.get('audioChannels') == 'No audio': 72 | encodefiltercommand+=',[outa]anullsink' 73 | 74 | with open(filterFilePath,'wb') as filterFile: 75 | filterFile.write(encodefiltercommand.encode('utf8')) 76 | 77 | targetWidth = 0 78 | targetHeight = 0 79 | 80 | try: 81 | widthCmd = ['ffmpeg']+inputsList+['-filter_complex_script',filterFilePath,'-frames:v','1','-f','null','-'] 82 | proc = sp.Popen(widthCmd,stdout=sp.PIPE,stderr=sp.PIPE) 83 | outs,errs = proc.communicate() 84 | for errLine in errs.split(b'\n'): 85 | for errElem in [x.strip() for x in errLine.split(b',')]: 86 | if b'x' in errElem: 87 | try: 88 | w,h = errElem.split(b' ')[0].split(b'x') 89 | w=int(w) 90 | h=int(h) 91 | targetWidth = w 92 | targetHeight = h 93 | except: 94 | pass 95 | except Exception as e: 96 | print(e) 97 | 98 | tileColumns = 0 99 | 100 | if not options.get('disableVP9Tiling',False): 101 | if targetWidth >= 960: 102 | tileColumns = 1 103 | if targetWidth >= 1920: 104 | tileColumns = 2 105 | if targetWidth >= 3840: 106 | tileColumns = 3 107 | 108 | print('VP9 targetWidth:',targetWidth) 109 | print('VP9 tileColumns:',tileColumns) 110 | 111 | if options.get('audioChannels') == 'No audio': 112 | ffmpegcommand+=['-filter_complex_script',filterFilePath] 113 | ffmpegcommand+=['-map','[outvfinal]'] 114 | elif 'Copy' in options.get('audioChannels',''): 115 | ffmpegcommand+=['-filter_complex_script',filterFilePath] 116 | ffmpegcommand+=['-map','[outvfinal]','-map','a:0'] 117 | else: 118 | ffmpegcommand+=['-filter_complex_script',filterFilePath] 119 | ffmpegcommand+=['-map','[outvfinal]','-map','[outa]'] 120 | 121 | if passPhase==1: 122 | ffmpegcommand+=['-pass', '1', '-passlogfile', logFilePath ] 123 | elif passPhase==2: 124 | ffmpegcommand+=['-pass', '2', '-passlogfile', logFilePath ] 125 | 126 | if bufsize is None: 127 | bufsize = 3000000 128 | if sizeLimitMax != 0.0 and not cqMode: 129 | bufsize = str(min(2000000000.0,br*2)) 130 | 131 | threadCount = globalOptions.get('encoderStageThreads',4) 132 | metadataSuffix = globalOptions.get('titleMetadataSuffix',' WmG') 133 | 134 | crf = 4 135 | if cqMode: 136 | crf = br 137 | br = 0 138 | 139 | audioCodec = ["-c:a","libopus"] 140 | if 'Copy' in options.get('audioChannels',''): 141 | audioCodec = [] 142 | 143 | 144 | 145 | if startEndTimestamps is None: 146 | ffmpegcommand+=["-shortest", "-copyts" 147 | ,"-start_at_zero", "-c:v","libvpx-vp9"] + audioCodec + [ 148 | "-stats","-pix_fmt","yuv420p" 149 | ,"-threads", str(threadCount) 150 | ,"-auto-alt-ref", "6", "-lag-in-frames", "25"] 151 | else: 152 | ffmpegcommand+=["-c:v","libvpx-vp9"] + audioCodec + [ 153 | "-stats","-pix_fmt","yuv420p" 154 | ,"-threads", str(threadCount) 155 | ,"-auto-alt-ref", "6", "-lag-in-frames", "25"] 156 | 157 | if not cqMode: 158 | #ffmpegcommand += ["-bufsize", str(bufsize)] 159 | pass 160 | 161 | if passPhase==1: 162 | pass 163 | else: 164 | ffmpegcommand += ['-speed', '1'] 165 | 166 | if options.get('forceBestDeadline',False): 167 | ffmpegcommand+=["-quality","best"] 168 | else: 169 | ffmpegcommand+=["-quality","good"] 170 | 171 | ffmpegcommand+=['-flags','+psnr', '-row-mt', '1', '-tile-columns', str(tileColumns), "-tile-rows", "0" 172 | ,"-arnr-maxframes", "7","-arnr-strength", "5" 173 | ,"-aq-mode", "0", "-tune-content", "film", "-enable-tpl", "1", "-frame-parallel", "0" 174 | ,"-metadata", 'Title={}'.format(filenamePrefix.replace('-','-') + metadataSuffix) ] 175 | 176 | 177 | qmaxOverride = 50 178 | useQmax=False 179 | try: 180 | temp = options.get('qmaxOverride',-1) 181 | if temp >= 0: 182 | qmaxOverride = temp 183 | useQmax=True 184 | except Exception as e: 185 | print(e) 186 | 187 | 188 | if sizeLimitMax == 0.0: 189 | if useQmax: 190 | ffmpegcommand+=["-b:v","0","-qmin","0","-qmax",str(qmaxOverride),"-crf" ,str(crf)] 191 | else: 192 | ffmpegcommand+=["-b:v","0","-crf" ,str(crf)] 193 | 194 | else: 195 | if useQmax: 196 | ffmpegcommand+=["-b:v",str(br),"-qmin","0","-qmax",str(qmaxOverride)] 197 | else: 198 | ffmpegcommand+=["-b:v",str(br)] 199 | 200 | if cqMode: 201 | ffmpegcommand+=["-crf", str(crf)] 202 | 203 | bitRateControl = options.get('bitRateControl','Average') 204 | 205 | if not useQmax and sizeLimitMax > 0.0: 206 | if bitRateControl == 'Limit Maximum': 207 | ffmpegcommand+=['-maxrate' ,str(br)] 208 | elif bitRateControl == 'Constant': 209 | ffmpegcommand+=['-minrate', str(br), '-maxrate' ,str(br)] 210 | 211 | if 'No audio' in options.get('audioChannels','') or passPhase==1: 212 | ffmpegcommand+=["-an"] 213 | elif 'Stereo' in options.get('audioChannels',''): 214 | ffmpegcommand+=["-ac","2"] 215 | ffmpegcommand+=["-ar",'48k'] 216 | ffmpegcommand+=["-b:a",str(audoBitrate)] 217 | elif 'Mono' in options.get('audioChannels',''): 218 | ffmpegcommand+=["-ac","1"] 219 | ffmpegcommand+=["-ar",'48k'] 220 | ffmpegcommand+=["-b:a",str(audoBitrate)] 221 | elif 'Copy' in options.get('audioChannels',''): 222 | ffmpegcommand+=["-c:a","copy"] 223 | else: 224 | ffmpegcommand+=["-an"] 225 | 226 | ffmpegcommand+=["-sn"] 227 | 228 | if passPhase==1: 229 | ffmpegcommand += ['-f', 'null', os.devnull] 230 | else: 231 | ffmpegcommand += [tempVideoFilePath] 232 | 233 | logging.debug("Ffmpeg command: {}".format(' '.join(ffmpegcommand))) 234 | 235 | print("Ffmpeg command: {}".format(' '.join(ffmpegcommand))) 236 | 237 | proc = sp.Popen(ffmpegcommand,stderr=sp.PIPE,stdin=sp.DEVNULL,stdout=sp.DEVNULL) 238 | 239 | if cqMode: 240 | encoderStatusCallback(None,None, lastEncodedCRF=crf, lastEncodedSize=None, lastBuff=0, lastWR=widthReduction) 241 | else: 242 | encoderStatusCallback(None,None, lastEncodedBR=br, lastEncodedSize=None, lastBuff=bufsize, lastWR=widthReduction) 243 | 244 | 245 | psnr, returnCode = logffmpegEncodeProgress(proc,'Pass {} {} {}'.format(passNumber,passReason,tempVideoFilePath),totalEncodedSeconds,totalExpectedEncodedSeconds,encoderStatusCallback,passNumber=passPhase,requestId=requestId,tempVideoPath=tempVideoFilePath,options=options) 246 | if isRquestCancelled(requestId): 247 | return 0, psnr, returnCode 248 | if passPhase==1: 249 | return 0, psnr, returnCode 250 | else: 251 | finalSize = os.stat(tempVideoFilePath).st_size 252 | encoderStatusCallback(None,None,lastEncodedSize=finalSize) 253 | return finalSize, psnr, returnCode 254 | 255 | encoderFunction.supportsCRQMode=True 256 | encoderStatusCallback('Encoding final '+videoFileName,(totalEncodedSeconds)/totalExpectedEncodedSeconds) 257 | 258 | minimumPSNR = 0.0 259 | try: 260 | minimumPSNR = float(options.get('minimumPSNR',0.0)) 261 | except: 262 | pass 263 | 264 | 265 | optimiser = encodeTargetingSize_linear 266 | if 'Nelder-Mead' in options.get('optimizer'): 267 | optimiser = encodeTargetingSize_nelder_mead 268 | 269 | finalFilenameConfirmed = optimiser(encoderFunction=encoderFunction, 270 | tempFilename=tempVideoFilePath, 271 | outputFilename=videoFilePath, 272 | initialDependentValue=initialBr, 273 | twoPassMode=True, 274 | allowEarlyExitWhenUndersize=globalOptions.get('allowEarlyExitIfUndersized',True), 275 | sizeLimitMin=sizeLimitMin, 276 | sizeLimitMax=sizeLimitMax, 277 | maxAttempts=globalOptions.get('maxEncodeAttempts',6), 278 | dependentValueMaximum=options.get('maximumBitrate',0), 279 | requestId=requestId, 280 | minimumPSNR=minimumPSNR, 281 | optimiserName=options.get('optimizer'), 282 | options=options, 283 | globalOptions=globalOptions) 284 | 285 | encoderStatusCallback('Encoding final '+videoFileName,(totalEncodedSeconds)/totalExpectedEncodedSeconds ) 286 | 287 | encoderStatusCallback('Encoding complete '+videoFilePath,1,finalFilename=finalFilenameConfirmed) -------------------------------------------------------------------------------- /src/encodingUtils.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | import threading 4 | import os 5 | import logging 6 | from datetime import datetime 7 | import math 8 | from collections import deque 9 | 10 | filesPlannedForCreation = set() 11 | fileExistanceLock = threading.Lock() 12 | 13 | cancelledEncodeIds = set() 14 | 15 | def isRquestCancelled(requestId): 16 | global cancelledEncodeIds 17 | return requestId in cancelledEncodeIds or -1 in cancelledEncodeIds 18 | 19 | packageglobalStatusCallback=print 20 | 21 | def idfunc(s):return s 22 | 23 | getShortPathName = idfunc 24 | 25 | try: 26 | import win32api 27 | getShortPathName=win32api.GetShortPathName 28 | except Exception as e: 29 | logging.error("win32api getShortPathName Exception", exc_info=e) 30 | 31 | def cleanFilenameForFfmpeg(filename): 32 | return getShortPathName(os.path.normpath(filename)) 33 | 34 | def cancelCurrentEncodeRequest(requestId): 35 | global cancelledEncodeIds 36 | cancelledEncodeIds.add(requestId) 37 | 38 | 39 | bit_depths = { 40 | "yuv420p": 8, "yuyv422": 8, "rgb24": 8, "bgr24": 8, "yuv422p": 8, "yuv444p": 8, "yuv410p": 8, 41 | "yuv411p": 8, "gray": 8, "monow": 1, "monob": 1, "pal8": 8, "yuvj420p": 8, "yuvj422p": 8, 42 | "yuvj444p": 8, "uyvy422": 8, "uyyvyy411": 8, "bgr8": 3, "bgr4": 1, "bgr4_byte": 1, "rgb8": 2, 43 | "rgb4": 1, "rgb4_byte": 1, "nv12": 8, "nv21": 8, "argb": 8, "rgba": 8, "abgr": 8, "bgra": 8, 44 | "gray16be": 16, "gray16le": 16, "yuv440p": 8, "yuvj440p": 8, "yuva420p": 8, "rgb48be": 16, 45 | "rgb48le": 16, "rgb565be": 5, "rgb565le": 5, "rgb555be": 5, "rgb555le": 5, "bgr565be": 5, 46 | "bgr565le": 5, "bgr555be": 5, "bgr555le": 5, "vaapi": 0, "yuv420p16le": 16, 47 | "yuv420p16be": 16, "yuv422p16le": 16, "yuv422p16be": 16, "yuv444p16le": 16, 48 | "yuv444p16be": 16, "dxva2_vld": 0, "rgb444le": 4, "rgb444be": 4, "bgr444le": 4, 49 | "bgr444be": 4, "ya8": 8, "bgr48be": 16, "bgr48le": 16, "yuv420p9be": 9, "yuv420p9le": 9, 50 | "yuv420p10be": 10, "yuv420p10le": 10, "yuv422p10be": 10, "yuv422p10le": 10, 51 | "yuv444p9be": 9, "yuv444p9le": 9, "yuv444p10be": 10, "yuv444p10le": 10, "yuv422p9be": 9, 52 | "yuv422p9le": 9, "gbrp": 8, "gbrp9be": 9, "gbrp9le": 9, "gbrp10be": 10, "gbrp10le": 10, 53 | "gbrp16be": 16, "gbrp16le": 16, "yuva422p": 8, "yuva444p": 8, "yuva420p9be": 9, 54 | "yuva420p9le": 9, "yuva422p9be": 9, "yuva422p9le": 9, "yuva444p9be": 9, "yuva444p9le": 9, 55 | "yuva420p10be": 10, "yuva420p10le": 10, "yuva422p10be": 10, "yuva422p10le": 10, 56 | "yuva444p10be": 10, "yuva444p10le": 10, "yuva420p16be": 16, "yuva420p16le": 16, 57 | "yuva422p16be": 16, "yuva422p16le": 16, "yuva444p16be": 16, "yuva444p16le": 16, 58 | "vdpau": 0, "xyz12le": 12, "xyz12be": 12, "nv16": 8, "nv20le": 10, "nv20be": 10, "rgba64be": 16, 59 | "rgba64le": 16, "bgra64be": 16, "bgra64le": 16, "yvyu422": 8, "ya16be": 16, "ya16le": 16, 60 | "gbrap": 8, "gbrap16be": 16, "gbrap16le": 16, "qsv": 0, "mmal": 0, "d3d11va_vld": 0, "cuda": 0, 61 | "0rgb": 8, "rgb0": 8, "0bgr": 8, "bgr0": 8, "yuv420p12be": 12, "yuv420p12le": 12, 62 | "yuv420p14be": 14, "yuv420p14le": 14, "yuv422p12be": 12, "yuv422p12le": 12, 63 | "yuv422p14be": 14, "yuv422p14le": 14, "yuv444p12be": 12, "yuv444p12le": 12, 64 | "yuv444p14be": 14, "yuv444p14le": 14, "gbrp12be": 12, "gbrp12le": 12, 65 | "gbrp14be": 14, "gbrp14le": 14, "yuvj411p": 8, "bayer_bggr8": 2, "bayer_rggb8": 2, 66 | "bayer_gbrg8": 2, "bayer_grbg8": 2, "bayer_bggr16le": 4, "bayer_bggr16be": 4, 67 | "bayer_rggb16le": 4, "bayer_rggb16be": 4, "bayer_gbrg16le": 4, "bayer_gbrg16be": 4, 68 | "bayer_grbg16le": 4, "bayer_grbg16be": 4, "xvmc": 0, "yuv440p10le": 10, "yuv440p10be": 10, 69 | "yuv440p12le": 12, "yuv440p12be": 12, "ayuv64le": 16, "ayuv64be": 16, "videotoolbox_vld": 0, 70 | "p010le": 10, "p010be": 10, "gbrap12be": 12, "gbrap12le": 12, "gbrap10be": 10, "gbrap10le": 10, 71 | "mediacodec": 0, "gray12be": 12, "gray12le": 12, "gray10be": 10, "gray10le": 10, "p016le": 16, 72 | "p016be": 16, "d3d11": 0, "gray9be": 9, "gray9le": 9, "gbrpf32be": 32, "gbrpf32le": 32, "gbrapf32be": 32, 73 | "gbrapf32le": 32, "drm_prime": 0, "opencl": 0, "gray14be": 14, "gray14le": 14, "grayf32be": 32, 74 | "grayf32le": 32, "yuva422p12be": 12, "yuva422p12le": 12, "yuva444p12be": 12, "yuva444p12le": 12, 75 | "nv24": 8, "nv42": 8, "vulkan": 0, "y210be": 10, "y210le": 10, "x2rgb10le": 10, "x2rgb10be": 10, 76 | "x2bgr10le": 10, "x2bgr10be": 10, "p210be": 10, "p210le": 10, "p410be": 10, "p410le": 10, "p216be": 16, 77 | "p216le": 16, "p416be": 16, "p416le": 16, "vuya": 8, "rgbaf16be": 16, "rgbaf16le": 16, "vuyx": 8, 78 | "p012le": 12, "p012be": 12, "y212be": 12, "y212le": 12, "xv30be": 10, "xv30le": 10, "xv36be": 12, 79 | "xv36le": 12, "rgbf32be": 32, "rgbf32le": 32, "rgbaf32be": 32, "rgbaf32le": 32 80 | } 81 | 82 | def getFreeNameForFileAndLog(filenamePrefix, extension, initialFileN=1): 83 | 84 | try: 85 | fileN=int(initialFileN) 86 | except Exception as e: 87 | print(e) 88 | fileN=1 89 | 90 | with fileExistanceLock: 91 | while True: 92 | 93 | videoFileName = '{}_{}.{}'.format(filenamePrefix, fileN, extension) 94 | outLogFilename = 'encoder_{}.log'.format(fileN) 95 | outFilterFilename = 'filters_{}.txt'.format(fileN) 96 | 97 | logFilePath = os.path.join('tempVideoFiles', outLogFilename) 98 | tempVideoFilePath = os.path.join('tempVideoFiles', videoFileName) 99 | filterFilePath = os.path.join('tempVideoFiles', outFilterFilename) 100 | videoFilePath = os.path.join('finalVideos', videoFileName) 101 | 102 | 103 | if not os.path.exists(tempVideoFilePath) and not os.path.exists(filterFilePath) and not os.path.exists(videoFilePath) and not os.path.exists(logFilePath) and videoFileName not in filesPlannedForCreation: 104 | filesPlannedForCreation.add(videoFileName) 105 | return videoFileName, logFilePath, filterFilePath, tempVideoFilePath, videoFilePath 106 | 107 | fileN+=1 108 | 109 | def logffmpegEncodeProgress(proc, processLabel, initialEncodedSeconds, totalExpectedEncodedSeconds, statusCallback, framesink=None, passNumber=0, requestId=None, tempVideoPath=None, options={}): 110 | currentEncodedTotal=0 111 | psnr = None 112 | ln=b'' 113 | logging.debug('Encode Start') 114 | 115 | earlyExitOnLowPSNR = options.get('earlyPSNRWidthReduction', False) 116 | 117 | minimumPSNR = -1 118 | try: 119 | minimumPSNR = int(float(options.get('minimumPSNR', -1))) 120 | except: 121 | pass 122 | 123 | earlyPSNRWindowLength = options.get('earlyPSNRWindowLength', 5) 124 | earlyPSNRSkip = options.get('earlyPSNRSkipSamples', 5) 125 | psnrSamplesSkipped = 0 126 | 127 | psnrQ = deque([], max(2, earlyPSNRWindowLength)) 128 | psnrAve = None 129 | outputSeen = False 130 | bpp = 8 131 | bit_depth_set = False 132 | while 1: 133 | try: 134 | if isRquestCancelled(requestId): 135 | proc.kill() 136 | outs, errs = proc.communicate() 137 | return 0, 0 138 | if proc.stderr is None: 139 | break 140 | c = proc.stderr.read(1) 141 | 142 | if len(c)==0: 143 | break 144 | if c == b'\r': 145 | 146 | if b'Output ' in ln: 147 | outputSeen = True 148 | 149 | print(ln) 150 | for p in ln.split(b' '): 151 | 152 | if b'Video:' in ln and b'Stream' in ln and outputSeen and not bit_depth_set: 153 | for key,v_depth in bit_depths.items(): 154 | if key.encode('utf8') in ln: 155 | bit_depth_set = True 156 | bpp = v_depth 157 | print('------------bpp',key,bpp) 158 | 159 | if b'*:' in p: 160 | try: 161 | tpsnr = float(p.split(b':')[-1]) 162 | if (not math.isnan(tpsnr)) and tpsnr != float('inf'): 163 | psnr = tpsnr 164 | 165 | if psnrSamplesSkipped > earlyPSNRSkip: 166 | psnrQ.append(psnr) 167 | 168 | psnrSamplesSkipped+=1 169 | 170 | if len(psnrQ) == earlyPSNRWindowLength and earlyExitOnLowPSNR: 171 | psnrAve = sum(psnrQ)/earlyPSNRWindowLength 172 | print('psnrAve', psnrAve, minimumPSNR, psnrQ) 173 | 174 | if psnrAve is not None and psnrAve < minimumPSNR: 175 | 176 | proc.kill() 177 | outs, errs = proc.communicate() 178 | statusCallback('Rolling PSNR too Low '+processLabel, 0, lastEncodedPSNR=psnr, encodeStage='PSNR Too Low', pix_fmt=bpp, encodePass='PSNR {} ({} samples)'.format(psnrAve, earlyPSNRWindowLength) ) 179 | return psnrAve, 1 180 | 181 | except Exception as e: 182 | logging.error("Encode capture psnr Exception", exc_info=e) 183 | if b'time=' in p: 184 | try: 185 | pt = datetime.strptime(p.split(b'=')[-1].decode('utf8'), '%H:%M:%S.%f') 186 | currentEncodedTotal = pt.microsecond/1000000 + pt.second + pt.minute*60 + pt.hour*3600 187 | if currentEncodedTotal>0: 188 | if passNumber == 0: 189 | statusCallback('Encoding '+processLabel, (currentEncodedTotal+initialEncodedSeconds)/totalExpectedEncodedSeconds, lastEncodedPSNR=psnr, pix_fmt=bpp, encodeStage='Encoding Final', encodePass='Single Pass Mode') 190 | elif passNumber == 1: 191 | statusCallback('Encoding '+processLabel, ((currentEncodedTotal/2)+initialEncodedSeconds)/totalExpectedEncodedSeconds, lastEncodedPSNR=psnr, pix_fmt=bpp, encodeStage='Encoding Final', encodePass='Two Pass Mode Pass 1' ) 192 | elif passNumber == 2: 193 | 194 | sizeNow = None 195 | 196 | print(tempVideoPath) 197 | try: 198 | if tempVideoPath is not None: 199 | sizeNow = os.stat(tempVideoPath).st_size 200 | except Exception as sze: 201 | print(sze) 202 | 203 | statusCallback('Encoding '+processLabel, ( ((totalExpectedEncodedSeconds-initialEncodedSeconds)/2) + (currentEncodedTotal/2)+initialEncodedSeconds)/totalExpectedEncodedSeconds, lastEncodedPSNR=psnr, encodeStage='Encoding Final', currentSize=sizeNow, encodePass='Two Pass Mode Pass 2' ) 204 | 205 | except ValueError as ve: 206 | logging.error("Awaiting timestamp in encode progress") 207 | except Exception as e: 208 | logging.error("Encode progress Exception", exc_info=e) 209 | ln=b'' 210 | ln+=c 211 | except Exception as e: 212 | logging.error("Encode progress Exception", exc_info=e) 213 | 214 | 215 | 216 | if framesink is not None: 217 | framesink.stdin.write(proc.stdout.read()) 218 | framesink.communicate() 219 | 220 | outs, errs = proc.communicate() 221 | 222 | if proc.returncode == 1: 223 | statusCallback('Encode Failed '+processLabel, 1, lastEncodedPSNR=psnr, encodeStage='Encode Failed', encodePass='Error code {}'.format(proc.returncode) ) 224 | 225 | if passNumber == 0: 226 | statusCallback('Complete '+processLabel, (currentEncodedTotal+initialEncodedSeconds)/totalExpectedEncodedSeconds, lastEncodedPSNR=psnr ) 227 | elif passNumber == 1: 228 | statusCallback('Complete '+processLabel, ((currentEncodedTotal/2)+initialEncodedSeconds)/totalExpectedEncodedSeconds, lastEncodedPSNR=psnr ) 229 | elif passNumber == 2: 230 | statusCallback('Complete '+processLabel, ( ((totalExpectedEncodedSeconds-initialEncodedSeconds)/2) + (currentEncodedTotal/2)+initialEncodedSeconds)/totalExpectedEncodedSeconds, lastEncodedPSNR=psnr ) 231 | 232 | return psnr, proc.returncode -------------------------------------------------------------------------------- /src/faceDetectionService.py: -------------------------------------------------------------------------------- 1 | 2 | from queue import Queue,Empty,Full 3 | import subprocess as sp 4 | import json 5 | import threading 6 | from .encodingUtils import cleanFilenameForFfmpeg 7 | 8 | class FaceDetectionService(): 9 | 10 | 11 | def __init__(self,globalStatusCallback=print,globalOptions={}): 12 | self.globalStatusCallback = globalStatusCallback 13 | self.faceDetectRequestQueue = Queue() 14 | self.globalOptions=globalOptions 15 | self.pigoPresent = False 16 | try: 17 | sp.run('pigo',stderr=sp.DEVNULL,stdout=sp.DEVNULL) 18 | self.pigoPresent=True 19 | except: 20 | self.pigoPresent = False 21 | self.cache = {} 22 | 23 | def processRectRequests(): 24 | if self.pigoPresent: 25 | while 1: 26 | sourceFile,filterStack,timestamp,callback = self.faceDetectRequestQueue.get() 27 | 28 | try: 29 | 30 | framePng = sp.run(['ffmpeg', '-ss', str(timestamp), '-i', cleanFilenameForFfmpeg(sourceFile), '-vframes', '1', '-c:v', 'png', '-f', 'image2pipe', '-'],stdout=sp.PIPE,stderr=sp.PIPE) 31 | rects = sp.run(['pigo', '-in', '-', '-cf', 'resources\\cascade\\facefinder', '-plc', 'resources\\cascade\\puploc', '-json', '-', '-out', 'empty'],input=framePng.stdout,stderr=sp.PIPE,stdout=sp.PIPE) 32 | print(rects) 33 | rects = json.loads(rects.stdout) 34 | 35 | for line in framePng.stderr.split(b'\n'): 36 | if b'Stream ' in line and b'Video:' in line: 37 | print(line) 38 | 39 | callback(sourceFile,timestamp,rects) 40 | except Exception as e: 41 | print(e) 42 | callback(sourceFile,timestamp,[]) 43 | 44 | self.faceDetectRequestQueue.task_done() 45 | 46 | self.faceWorkerThread = threading.Thread(target=processRectRequests,daemon=True) 47 | self.faceWorkerThread.start() 48 | 49 | def clearCache(self): 50 | self.cache = {} 51 | 52 | def faceDetectEnabled(self): 53 | return self.pigoPresent 54 | 55 | def getFaceBoundingRect(self,sourceFile,filterStack,timestamp,callback): 56 | if self.pigoPresent: 57 | self.faceDetectRequestQueue.put( (sourceFile,filterStack,timestamp,callback) ) 58 | else: 59 | callback([]) 60 | 61 | if __name__ == '__main__': 62 | fd = FaceDetectionService() 63 | def cb(sourceFile,timestamp,rect): 64 | print(sourceFile,timestamp,rect) 65 | 66 | fd.getFaceBoundingRect("C:\\Users\\baxter001\\VideoEditor\\resources\\_-ph5f3d22d619f78_Katekuray_1_2.webm",'',10,cb) 67 | fd.faceDetectRequestQueue.join() -------------------------------------------------------------------------------- /src/ffmpegInfoParser.py: -------------------------------------------------------------------------------- 1 | import subprocess as sp 2 | from dataclasses import dataclass 3 | import logging 4 | 5 | @dataclass 6 | class VideoInfo: 7 | filename: str 8 | duration: float 9 | fps: float 10 | tbr: float 11 | tbn: float 12 | height:int 13 | width:int 14 | hasaudio:bool 15 | 16 | def getVideoInfo(filename,filters=None): 17 | state=None 18 | stats= dict(filename=filename,duration=0,hasaudio=False,fps=24,tbr=None,tbn=None,height=0,width=0) 19 | if filters is None: 20 | proc = sp.Popen(['ffmpeg','-i',filename],stdout=sp.PIPE,stderr=sp.PIPE) 21 | else: 22 | proc = sp.Popen(['ffmpeg','-i',filename,'-filter_complex', filters,'-frames:v','1','-f','null','-'],stdout=sp.PIPE,stderr=sp.PIPE) 23 | 24 | outs,errs = proc.communicate() 25 | for errLine in errs.split(b'\n'): 26 | for errElem in [x.strip() for x in errLine.split(b',')]: 27 | if errElem.startswith(b'Duration:'): 28 | try: 29 | timeParts = [float(x) for x in errElem.split()[-1].split(b':')] 30 | stats['duration'] = sum([t*m for t,m in zip(timeParts[::-1],[1,60,60*60,60*60*60])]) 31 | except Exception as e: 32 | logging.error("getVideoInfo Exception",exc_info=e) 33 | elif errElem.startswith(b'Stream'): 34 | if b'Video:' in errElem: 35 | state='Video' 36 | elif b'Audio:' in errElem: 37 | state = 'Audio' 38 | stats['hasaudio'] = True 39 | elif state=='Video': 40 | if errElem.endswith(b'fps'): 41 | try: 42 | stats['fps']=float(errElem.split(b' ')[0]) 43 | except Exception as e: 44 | logging.error("getVideoInfo Exception",exc_info=e) 45 | elif errElem.endswith(b'tbr'): 46 | try: 47 | mult=1 48 | temptbrerr = errElem.split(b' ')[0] 49 | if b'k' in temptbrerr: 50 | temptbrerr = temptbrerr.replace(b'k',b'') 51 | mult=1000 52 | stats['tbr']=float(temptbrerr)*mult 53 | except Exception as e: 54 | logging.error("getVideoInfo Exception",exc_info=e) 55 | elif errElem.endswith(b'tbn') or b' tbn' in errElem: 56 | try: 57 | mult=1 58 | temptbnerr = errElem.split(b' ')[0] 59 | if b'k' in temptbnerr: 60 | temptbnerr = temptbnerr.replace(b'k',b'') 61 | mult=1000 62 | stats['tbn']=float(temptbnerr)*mult 63 | except Exception as e: 64 | logging.error("getVideoInfo Exception",exc_info=e) 65 | elif errElem.endswith(b'tbn'): 66 | try: 67 | tbnval = errElem.split(b' ')[0] 68 | if b'k' in tbnval: 69 | stats['tbn']=float(tbnval.replace(b'k',b'000')) 70 | else: 71 | stats['tbn']=float(tbnval) 72 | except Exception as e: 73 | logging.error("getVideoInfo Exception",exc_info=e) 74 | if b'x' in errElem: 75 | try: 76 | w,h = errElem.split(b' ')[0].split(b'x') 77 | w=int(w) 78 | h=int(h) 79 | 80 | stats['height'] = h 81 | stats['width'] = w 82 | except: 83 | pass 84 | 85 | return VideoInfo(**stats) 86 | 87 | if __name__ == '__main__': 88 | import webmGenerator -------------------------------------------------------------------------------- /src/masonry.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | from math import floor 4 | 5 | class Brick: 6 | 7 | def __init__(self,identifier,width,height): 8 | self.identifier=identifier 9 | self.height=height*1.0 10 | self.width=width*1.0 11 | 12 | def __repr__(self): 13 | return "".format(self.identifier,self.width,self.height) 14 | 15 | def getSize(self): 16 | return self.width,self.height 17 | 18 | def getSizeWithContstraint(self,constrainedDirection,constraint,logger=None,xo=None,yo=None,padding=0): 19 | if constraint is None: 20 | if logger is not None: 21 | logger[self.identifier]=(floor(xo)+padding, 22 | floor(yo)+padding, 23 | floor(self.width)-(padding*2), 24 | floor(self.height)-(padding*2), 25 | 1.0, 26 | floor(self.width)-(padding*2), 27 | floor(self.height)-(padding*2)) 28 | 29 | return floor(self.width),floor(self.height) 30 | else: 31 | if constrainedDirection=='height': 32 | ar = constraint/self.height 33 | if logger is not None: 34 | logger[self.identifier]=(floor(xo)+padding, 35 | floor(yo)+padding, 36 | floor(self.width*ar)-(padding*2), 37 | floor(constraint)-(padding*2), 38 | ar, 39 | floor(self.width)-(padding*2), 40 | floor(self.height)-(padding*2)) 41 | 42 | return floor(self.width*ar),floor(constraint) 43 | 44 | elif constrainedDirection=='width': 45 | ar = constraint/self.width 46 | if logger is not None: 47 | logger[self.identifier]=(floor(xo)+padding, 48 | floor(yo)+padding, 49 | floor(constraint)-(padding*2), 50 | floor(self.height*ar)-(padding*2), 51 | ar, 52 | floor(self.width)-(padding*2), 53 | floor(self.height)-(padding*2)) 54 | return floor(constraint),floor(self.height*ar) 55 | else: 56 | raise Exception('Invalid direction {}'.format(constrainedDirection)) 57 | 58 | class Stack: 59 | 60 | def __init__(self,bricks,orientation='vertical'): 61 | if orientation not in ('vertical','horizontal'): 62 | raise Exception('Invalid orientation') 63 | self.bricks=bricks 64 | self.orientation=orientation 65 | 66 | def __init__(self,bricks,orientation='vertical'): 67 | if orientation not in ('vertical','horizontal'): 68 | raise Exception('Invalid orientation') 69 | self.bricks=bricks 70 | self.orientation=orientation 71 | 72 | def append(self,brick): 73 | self.bricks.append(brick) 74 | 75 | def insert(self,pos,brick): 76 | self.bricks.insert(pos,brick) 77 | 78 | def __repr__(self): 79 | return "".format(self.orientation,len(self.bricks)) 80 | 81 | 82 | def getSizeWithContstraint(self,direction,constraint,logger=None,xo=None,yo=None,padding=0): 83 | if direction=='height': 84 | if self.orientation=='vertical': 85 | heights=[] 86 | for brick in self.bricks: 87 | _,h = brick.getSizeWithContstraint('width',1000) 88 | heights.append(h) 89 | sumheights=sum(heights) 90 | 91 | finalwidth=0 92 | finalheight=0 93 | heights = [(h/sumheights)*constraint for h in heights] 94 | for requestedHeight,brick in zip(heights,self.bricks): 95 | w,h = brick.getSizeWithContstraint('height',requestedHeight,logger,xo,None if yo is None else yo+finalheight,padding=padding) 96 | finalwidth=w 97 | finalheight+=h 98 | 99 | return floor(finalwidth),floor(finalheight) 100 | elif self.orientation=='horizontal': 101 | finalwidth=0 102 | finalheight=0 103 | for brick in self.bricks: 104 | w,h = brick.getSizeWithContstraint('height',constraint,logger,None if xo is None else xo+finalwidth,yo,padding=padding) 105 | finalwidth+=w 106 | finalheight=h 107 | return floor(finalwidth),floor(finalheight) 108 | elif direction=='width': 109 | if self.orientation=='horizontal': 110 | widths=[] 111 | for brick in self.bricks: 112 | w,_ = brick.getSizeWithContstraint('height',1000,padding=padding) 113 | widths.append(w) 114 | sumwidths=sum(widths) 115 | 116 | finalwidth=0 117 | finalheight=0 118 | widths = [(w/sumwidths)*constraint for w in widths] 119 | 120 | for requestedWidth,brick in zip(widths,self.bricks): 121 | w,h = brick.getSizeWithContstraint('width',requestedWidth,logger,None if xo is None else xo+finalwidth,yo,padding=padding) 122 | finalwidth+=w 123 | finalheight=h 124 | return floor(finalwidth),floor(finalheight) 125 | elif self.orientation=='vertical': 126 | finalwidth=0 127 | finalheight=0 128 | for brick in self.bricks: 129 | w,h = brick.getSizeWithContstraint('width',constraint,logger,xo,None if yo is None else yo+finalheight,padding=padding) 130 | finalwidth=w 131 | finalheight+=h 132 | return floor(finalwidth),floor(finalheight) 133 | else: 134 | raise Exception('Invalid direction') 135 | -------------------------------------------------------------------------------- /src/mergeSelectionController.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import json 4 | 5 | class MergeSelectionController: 6 | 7 | def __init__(self,ui,videoManager,ffmpegService,filterController,cutController,controller,globalOptions={}): 8 | self.ui=ui 9 | self.globalOptions=globalOptions 10 | self.videoManager=videoManager 11 | self.ffmpegService=ffmpegService 12 | self.filterController=filterController 13 | self.cutController=cutController 14 | self.profileSpecPath = 'customEncodeprofiles' 15 | self.controller = controller 16 | self.maxAutoconvert = -1 17 | 18 | self.stdProfileSpecs = [ 19 | {'name':'None','editable':False}, 20 | {'name':'Default max quality mp4','editable':False,'outputFormat':'mp4:x264','maximumSize':'0.0'}, 21 | {'name':'Sub 4M max quality vp8 webm','editable':False,'outputFormat':'webm:VP8','maximumSize':'4.0'}, 22 | {'name':'Sub 100M max quality mp4','editable':False,'outputFormat':'mp4:x264','maximumSize':'100.0'} 23 | ] 24 | 25 | self.customProfileSpecs = [ 26 | {'name':'Sub 8M max quality mp4','editable':False,'outputFormat':'mp4:x264','maximumSize':'8.0'} 27 | ] 28 | 29 | if os.path.exists(self.profileSpecPath): 30 | for profileFile in os.listdir(self.profileSpecPath): 31 | profileFilename = os.path.join(self.profileSpecPath,profileFile) 32 | if os.path.exists(profileFilename) and os.path.isfile(profileFilename): 33 | try: 34 | profile = json.loads( open(profileFilename,'r').read() ) 35 | profile['filename'] = profileFile 36 | profile['editable'] = True 37 | profileName = profile['name'] 38 | profile['name'] = profileName 39 | self.customProfileSpecs.append( profile ) 40 | except Exception as e: 41 | print('Custom profile load error',profileFilename,e) 42 | 43 | self.ui.setController(self) 44 | self.videoManager.addSubclipChangeCallback(self.ui.videoSubclipDurationChangeCallback) 45 | 46 | def getLabelForRid(self, rid): 47 | return self.videoManager.getLabelForClip('',rid) 48 | 49 | def getSeqGroupForRid(self, rid): 50 | return self.videoManager.getSeqGroupForClip('',rid) 51 | 52 | def autoConvert(self): 53 | self.ui.updateSelectableVideos() 54 | self.ui.clearSequence(includeProgress=False) 55 | self.maxAutoconvert = self.ui.addAllClipsInTimelineOrder(minrid=self.maxAutoconvert,clearProgress=False) 56 | self.ui.encodeCurrent() 57 | self.ui.clearSequence(includeProgress=False) 58 | 59 | def setIgnoreDrop(self,path): 60 | self.controller.setIgnoreDrop(path) 61 | 62 | def setDragDur(self,dur): 63 | self.cutController.setDragDur(dur) 64 | 65 | def broadcastModalFocus(self): 66 | self.cutController.playingModalGotFocus() 67 | 68 | def broadcastModalLoseFocus(self): 69 | self.cutController.playingModalLostFocus() 70 | 71 | def jumpToFilterByRid(self,rid): 72 | self.filterController.jumpToFilterByRid(rid) 73 | 74 | def updateSubclipBoundry(self,subclip,originalts,ts,pos,towardsMiddle=True): 75 | 76 | filename,s,e = self.videoManager.getDetailsForRangeId(subclip.rid) 77 | 78 | if pos == 's': 79 | newPosSeconds = s-(originalts-ts) 80 | else: 81 | newPosSeconds = e-(originalts-ts) 82 | 83 | newPosSeconds = max(0,newPosSeconds) 84 | 85 | if towardsMiddle: 86 | halfDiff = (originalts-ts)/2 87 | if pos == 's': 88 | self.videoManager.updatePointForClip(filename,subclip.rid,'s',s-halfDiff) 89 | self.videoManager.updatePointForClip(filename,subclip.rid,'e',e+halfDiff) 90 | elif pos == 'e': 91 | self.videoManager.updatePointForClip(filename,subclip.rid,'s',s+halfDiff) 92 | self.videoManager.updatePointForClip(filename,subclip.rid,'e',e-halfDiff) 93 | else: 94 | self.videoManager.updatePointForClip(filename,subclip.rid,pos,newPosSeconds) 95 | 96 | def synchroniseCutController(self,rid,startoffset,forceTabJump=False): 97 | self.cutController.jumpToRidAndOffset(rid,startoffset,forceTabJump) 98 | 99 | def getDefaultPostFilter(self): 100 | return self.globalOptions.get('defaultPostProcessingFilter','') 101 | 102 | def getFilteredClips(self, mode): 103 | if mode == 'CLIPS': 104 | return self.filterController.getClipsWithFilters() 105 | else: 106 | videos = [] 107 | n=90001 108 | for filename in self.cutController.files: 109 | n+=1 110 | videos.append((filename,n,0,1,'null', 'null', 'null')) 111 | return videos 112 | 113 | 114 | def requestPreviewFrame(self,rid,filename,timestamp,filterexp,size,callback): 115 | print('requestPreviewFrame',rid,filename,timestamp) 116 | self.ffmpegService.requestPreviewFrame(rid,filename,timestamp,filterexp,size,callback) 117 | 118 | def encode(self,requestId,mode,seq,options,filenamePrefix,statusCallback): 119 | print('encode',requestId,mode,seq,options,filenamePrefix) 120 | self.ffmpegService.encode(requestId,mode,seq,options,filenamePrefix,statusCallback) 121 | 122 | def cancelEncodeRequest(self,requestId): 123 | self.ffmpegService.cancelEncodeRequest(requestId) 124 | 125 | def registerComplete(self,filename,clip=None): 126 | self.controller.registerComplete(filename,clip=clip) 127 | 128 | def deleteCustomProfile(self,profileName): 129 | return '' 130 | 131 | def saveCustomProfile(self,profile): 132 | os.path.exists(self.profileSpecPath) or os.mkdir(self.profileSpecPath) 133 | return '' 134 | 135 | def getProfiles(self): 136 | return self.stdProfileSpecs + self.customProfileSpecs 137 | 138 | def close_ui(self): 139 | try: 140 | self.ui.close_ui() 141 | except Exception as e: 142 | print(e) 143 | 144 | if __name__ == '__main__': 145 | import webmGenerator 146 | -------------------------------------------------------------------------------- /src/optimisers/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/optimisers/linear.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | import shutil 4 | import logging 5 | 6 | from ..encodingUtils import isRquestCancelled 7 | 8 | def encodeTargetingSize(encoderFunction,tempFilename,outputFilename,initialDependentValue,sizeLimitMin,sizeLimitMax,maxAttempts,allowEarlyExitWhenUndersize=True,twoPassMode=False,dependentValueName='BR',dependentValueMaximum=0,requestId=None,minimumPSNR=0.0,optimiserName='',options={},globalOptions={}): 9 | 10 | val = initialDependentValue 11 | targetSizeMedian = (sizeLimitMin+sizeLimitMax)/2 12 | smallestFailedOverMaximum=None 13 | largestFailedUnderMinimum=None 14 | passCount=0 15 | lastFailReason='' 16 | passReason='Initial Pass' 17 | lastpsnr = None 18 | psnrAdjusted=False 19 | widthReduction = 0.0 20 | 21 | print('------') 22 | print('sizeLimitMax',sizeLimitMax) 23 | print('targetSizeMedian',targetSizeMedian) 24 | print('sizeLimitMin',sizeLimitMin) 25 | print('dependentValueMaximum',dependentValueMaximum) 26 | print('val',val) 27 | print('------') 28 | 29 | while 1: 30 | val=int(val) 31 | passCount+=1 32 | 33 | if dependentValueMaximum is not None and dependentValueMaximum > 0: 34 | val = min(val,dependentValueMaximum) 35 | 36 | if isRquestCancelled(requestId): 37 | return 38 | 39 | cqMode = options.get('cqMode',False) 40 | try: 41 | cqMode = cqMode and encoderFunction.supportsCRQMode 42 | except: 43 | cqMode=False 44 | 45 | if cqMode and passCount == 1: 46 | val=10 47 | if cqMode: 48 | val = int(min(max(val,4),50)) 49 | 50 | if twoPassMode: 51 | passReason='Stats Pass {} {}'.format(passCount+1,lastFailReason) 52 | _ = encoderFunction(val,passCount,passReason,passPhase=1,requestId=requestId,widthReduction=widthReduction,cqMode=cqMode) 53 | passReason='Encode Pass {} {}'.format(passCount+1,lastFailReason) 54 | finalSize,lastpsnr,returnCode = encoderFunction(val,passCount,passReason,passPhase=2,requestId=requestId,widthReduction=widthReduction,cqMode=cqMode) 55 | else: 56 | passReason='Encode Pass {} {}'.format(passCount+1,lastFailReason) 57 | finalSize,lastpsnr,returnCode = encoderFunction(val,passCount,passReason,requestId=requestId,widthReduction=widthReduction,cqMode=cqMode) 58 | 59 | if isRquestCancelled(requestId): 60 | return 61 | 62 | adjustval = True 63 | 64 | widthReductionFactor = globalOptions.get('defaultPSNRWidthReductionFactor',0.5) 65 | 66 | if lastpsnr is not None and lastpsnr < minimumPSNR and (widthReduction+widthReductionFactor)<=1.0: 67 | widthReduction+=widthReductionFactor 68 | maxAttempts += passCount 69 | adjustval=False 70 | psnrAdjusted=True 71 | lastFailReason = 'PSNR under {} ({}), resetting at smaller dimensions'.format(minimumPSNR,lastpsnr) 72 | continue 73 | elif returnCode == 1: 74 | return 75 | elif sizeLimitMinmaxAttempts and finalSizesizeLimitMax: 86 | lastFailReason = 'File too large {:.2%}, CRQ increase'.format(finalSize/sizeLimitMax) 87 | if largestFailedUnderMinimum is None or val>largestFailedUnderMinimum: 88 | largestFailedUnderMinimum=val 89 | 90 | elif finalSizelargestFailedUnderMinimum: 93 | largestFailedUnderMinimum=val 94 | elif finalSize>sizeLimitMax: 95 | lastFailReason = 'File too large {:.2%}, {} decrease'.format(finalSize/sizeLimitMax,dependentValueName) 96 | if smallestFailedOverMaximum is None or val= max_iter: 62 | print(lastEncodeParams,res[0]) 63 | if (lastEncodeParams == res[0][0]).all(): 64 | return res[0] 65 | else: 66 | f(res[0],**extra_args) 67 | return res[0] 68 | iters += 1 69 | 70 | if best < prev_best - no_improve_thr: 71 | no_improv = 0 72 | prev_best = best 73 | else: 74 | no_improv += 1 75 | 76 | if no_improv >= no_improv_break: 77 | print(lastEncodeParams,res[0]) 78 | if (lastEncodeParams == res[0][0]).all(): 79 | return res[0] 80 | else: 81 | f(res[0]) 82 | return res[0] 83 | 84 | # centroid 85 | x0 = [0.] * dim 86 | for tup in res[:-1]: 87 | for i, c in enumerate(tup[0]): 88 | x0[i] += c / (len(res)-1) 89 | 90 | # reflection 91 | xr = x0 + alpha*(x0 - res[-1][0]) 92 | xr = contstrain(xr, x_lower, x_upper) 93 | rscore,isAcceptable = f(xr,**extra_args) 94 | lastEncodeParams = xr 95 | if isAcceptable and iters >= min_iter_before_acceptable: 96 | return xr 97 | if res[0][1] <= rscore < res[-2][1]: 98 | del res[-1] 99 | res.append([xr, rscore]) 100 | continue 101 | 102 | # expansion 103 | if rscore < res[0][1]: 104 | xe = x0 + gamma*(x0 - res[-1][0]) 105 | xe = contstrain(xe, x_lower, x_upper) 106 | escore,isAcceptable = f(xe,**extra_args) 107 | lastEncodeParams = xe 108 | if isAcceptable and iters >= min_iter_before_acceptable: 109 | return xe 110 | if escore < rscore: 111 | del res[-1] 112 | res.append([xe, escore]) 113 | continue 114 | else: 115 | del res[-1] 116 | res.append([xr, rscore]) 117 | continue 118 | 119 | # contraction 120 | xc = x0 + rho*(x0 - res[-1][0]) 121 | xc = contstrain(xc, x_lower, x_upper) 122 | cscore,isAcceptable = f(xc,**extra_args) 123 | lastEncodeParams = xc 124 | if isAcceptable and iters >= min_iter_before_acceptable: 125 | return xc 126 | if cscore < res[-1][1]: 127 | del res[-1] 128 | res.append([xc, cscore]) 129 | continue 130 | 131 | # reduction 132 | x1 = res[0][0] 133 | nres = [] 134 | for tup in res: 135 | redx = x1 + sigma*(tup[0] - x1) 136 | redx = contstrain(redx, x_lower, x_upper) 137 | score,isAcceptable = f(redx,**extra_args) 138 | lastEncodeParams = redx 139 | if isAcceptable and iters >= min_iter_before_acceptable: 140 | return xc 141 | nres.append([redx, score]) 142 | res = nres 143 | 144 | 145 | def encodeTargetingSize(encoderFunction,tempFilename,outputFilename,initialDependentValue,sizeLimitMin,sizeLimitMax,maxAttempts,allowEarlyExitWhenUndersize=True,twoPassMode=False,dependentValueName='BR',dependentValueMaximum=0,requestId=None,minimumPSNR=0.0,optimiserName='Nelder-Mead - Early Exit',options={},globalOptions={}): 146 | val = initialDependentValue 147 | targetSizeMedian = (sizeLimitMin+sizeLimitMax)/2 148 | smallestFailedOverMaximum=None 149 | largestFailedUnderMinimum=None 150 | passCount=0 151 | lastFailReason='' 152 | passReason='Initial Pass' 153 | widthReduction = 0.0 154 | 155 | 156 | if minimumPSNR > 0.0: 157 | min_iter_before_acceptable = 3 158 | 159 | x_start = np.array([initialDependentValue,0.0]) 160 | x_upper = np.array([initialDependentValue*1.9,0.9]) 161 | x_lower = np.array([initialDependentValue/2,0.0]) 162 | 163 | x_step = [initialDependentValue*0.1,0.01] 164 | x_argNames = {'argNames':['br','wr']} 165 | elif 'Exhaustive' in optimiserName: 166 | min_iter_before_acceptable = float('inf') 167 | max_iter = 0 168 | 169 | x_start = np.array([initialDependentValue,0.0,initialDependentValue*2]) 170 | x_upper = np.array([initialDependentValue*1.9,0.9,initialDependentValue*2]) 171 | x_lower = np.array([initialDependentValue/2,0.0,initialDependentValue/5]) 172 | 173 | x_step = [initialDependentValue*0.1,0.01,initialDependentValue*0.1] 174 | x_argNames = {'argNames':['br','wr','buf']} 175 | else: 176 | min_iter_before_acceptable = 1 177 | 178 | x_start = np.array([initialDependentValue]) 179 | x_upper = np.array([initialDependentValue*1.9]) 180 | x_lower = np.array([initialDependentValue/2]) 181 | 182 | x_step = [initialDependentValue*0.1] 183 | x_argNames = {'argNames':['br']} 184 | 185 | max_iter = 15 186 | if 'Exhaustive' in optimiserName: 187 | min_iter_before_acceptable = float('inf') 188 | max_iter = 0 189 | 190 | def encodeOptimizationWrapper(x,argNames=None): 191 | nonlocal passCount 192 | 193 | passCount+=1 194 | 195 | widthReduction = 0.0 196 | buff=None 197 | bitrate=0.0 198 | 199 | 200 | if argNames == ['br']: 201 | bitrate = x[0] 202 | widthReduction=0.0 203 | buff=None 204 | elif argNames == ['br','wr']: 205 | bitrate = x[0] 206 | widthReduction=x[1] 207 | buff=None 208 | elif argNames == ['br','wr','buf']: 209 | bitrate = x[0] 210 | widthReduction=x[1] 211 | buff=x[2] 212 | 213 | 214 | if widthReduction >= 0.9: 215 | widthReduction = 0.9 216 | elif widthReduction <= 0.0: 217 | widthReduction = 0.0 218 | 219 | bitrate = int(bitrate) 220 | widthReduction = round(widthReduction,3) 221 | 222 | if buff is not None: 223 | buff=int(buff) 224 | 225 | if twoPassMode: 226 | passReason='Nelder-Mead Stats Pass {} {}'.format(passCount+1,lastFailReason) 227 | _ = encoderFunction(bitrate,passCount,passReason,passPhase=1,requestId=requestId,widthReduction=widthReduction,bufsize=buff) 228 | passReason='Nelder-Mead Encode Pass {} {}'.format(passCount+1,lastFailReason) 229 | finalSize,lastpsnr, returnCode = encoderFunction(bitrate,passCount,passReason,passPhase=2,requestId=requestId,widthReduction=widthReduction,bufsize=buff) 230 | else: 231 | passReason='Nelder-Mead Encode Pass {} {}'.format(passCount+1,lastFailReason) 232 | finalSize,lastpsnr, returnCode = encoderFunction(bitrate,passCount,passReason,requestId=requestId,widthReduction=widthReduction,bufsize=buff) 233 | 234 | try: 235 | if lastpsnr is not None and lastpsnr != float('inf'): 236 | psnrScore = (52-lastpsnr)/52 237 | elif lastpsnr == float('inf'): 238 | psnrScore = 0.0 239 | else: 240 | psnrScore = 1 241 | 242 | widthReductionScore = widthReduction 243 | 244 | sizeScore = abs(finalSize-sizeLimitMax)/sizeLimitMax 245 | if finalSize > sizeLimitMax: 246 | sizeScore = abs(abs(finalSize-sizeLimitMax)/sizeLimitMax)*100 247 | bitrateScore = (initialDependentValue*2)/bitrate 248 | 249 | score = psnrScore+widthReductionScore+sizeScore+bitrateScore 250 | except Exception as e: 251 | print(e) 252 | score = float('inf') 253 | 254 | 255 | 256 | print("Optimzation pass complete - score: {} bitrate:{} widthReduction:{} finalSize:{} psnr:{} ".format(score,bitrate,widthReduction,finalSize,lastpsnr)) 257 | 258 | isAcceptable = (sizeLimitMin minimumPSNR ) 259 | 260 | return score,isAcceptable 261 | 262 | 263 | x = nelder_mead(encodeOptimizationWrapper, x_start, x_upper=x_upper, x_lower=x_lower, step=x_step, extra_args=x_argNames, max_iter=15, min_iter_before_acceptable=min_iter_before_acceptable) 264 | shutil.move(tempFilename,outputFilename) 265 | return outputFilename -------------------------------------------------------------------------------- /src/screenspacetools.lua: -------------------------------------------------------------------------------- 1 | local msg = require('mp.msg') 2 | local assdraw = require('mp.assdraw') 3 | 4 | local script_name = "screenspacetools" 5 | local regmarks = {}; 6 | 7 | local registrationAss = ""; 8 | local mouseAss = ""; 9 | local sketchAss = ""; 10 | local cropAss = ""; 11 | local vectorAss = ""; 12 | 13 | 14 | local ass_set_color = function (idx, color) 15 | assert(color:len() == 8 or color:len() == 6) 16 | local ass = "" 17 | -- Set alpha value (if present) 18 | if color:len() == 8 then 19 | local alpha = 0xff - tonumber(color:sub(7, 8), 16) 20 | ass = ass .. string.format("\\%da&H%X&", idx, alpha) 21 | end 22 | -- Swizzle RGB to BGR and build ASS string 23 | color = color:sub(5, 6) .. color:sub(3, 4) .. color:sub(1, 2) 24 | return "{" .. ass .. string.format("\\%dc&H%s&", idx, color) .. "}" 25 | end 26 | 27 | 28 | function screenspacetools_clear(p1x,p1y,p2x,p2y,fill,border,width,visible) 29 | cropAss=""; 30 | mouseAss = ""; 31 | draw_merged_ssa() 32 | end 33 | 34 | function screenspacetools_drawVector(x1,y1,x2,y2) 35 | if x1==0 and y1==0 and x2 ==0 and y2 ==0 then 36 | vectorAss = ""; 37 | else 38 | local osd_w, osd_h = mp.get_property("osd-width"), mp.get_property("osd-height") 39 | 40 | ass = assdraw.ass_new() 41 | ass:pos(0,0) 42 | ass:new_event() 43 | ass:draw_start() 44 | ass:pos(0,0) 45 | 46 | ass:append(ass_set_color(1, "00000000")) 47 | ass:append(ass_set_color(3, "69dbdbff")) 48 | ass:append("{\\bord0.5}") 49 | 50 | ass:move_to(tonumber(x1-4), tonumber(y1-4)) 51 | ass:rect_cw(x1-4,y1-4,x1+4,y1+4) 52 | 53 | ass:move_to(tonumber(x1), tonumber(y1)) 54 | ass:line_to(tonumber(x2), tonumber(y2)) 55 | 56 | ass:move_to(tonumber(x2-10), tonumber(y2)) 57 | ass:line_to(tonumber(x2+10), tonumber(y2)) 58 | 59 | ass:move_to(tonumber(x2), tonumber(y2-10)) 60 | ass:line_to(tonumber(x2), tonumber(y2+10)) 61 | 62 | ass:draw_stop() 63 | ass:pos(0,0) 64 | vectorAss = ass.text; 65 | 66 | end 67 | 68 | draw_merged_ssa() 69 | 70 | end 71 | 72 | function screenspacetools_regMark(x,y,type) 73 | 74 | if type == "clear" then 75 | regmarks = {}; 76 | elseif type ~= "" then 77 | table.insert(regmarks, {px=x,py=y,t=type}); 78 | end 79 | 80 | 81 | local osd_w, osd_h = mp.get_property("osd-width"), mp.get_property("osd-height") 82 | 83 | ass = assdraw.ass_new() 84 | 85 | 86 | 87 | 88 | for k, v in pairs(regmarks) do 89 | 90 | ass:pos(0,0) 91 | ass:new_event() 92 | ass:draw_start() 93 | ass:pos(0,0) 94 | 95 | ass:append(ass_set_color(1, "00000000")) 96 | if v["t"] == "tvec" then 97 | ass:append(ass_set_color(3, "ff0000ff")) 98 | ass:append("{\\bord0.2}") 99 | else 100 | ass:append(ass_set_color(3, "69dbdbff")) 101 | ass:append("{\\bord0.5}") 102 | end 103 | 104 | if v["t"] == "cross" or v["t"] == "tvec" then 105 | ass:move_to(tonumber(0), tonumber(v["py"])) 106 | ass:line_to(tonumber(osd_w), tonumber(v["py"])) 107 | ass:move_to(tonumber(v["px"]), tonumber(0)) 108 | ass:line_to(tonumber(v["px"]), tonumber(osd_h)) 109 | end 110 | 111 | if v["t"] == "cross" or v["t"] == "tvec" then 112 | ass:move_to(tonumber(v["px"]-15), tonumber(v["py"])) 113 | ass:line_to(tonumber(v["px"]+15), tonumber(v["py"])) 114 | ass:move_to(tonumber(v["px"]), tonumber(v["py"]-15)) 115 | ass:line_to(tonumber(v["px"]), tonumber(v["py"]+15)) 116 | end 117 | 118 | 119 | if v["t"] == "vline" then 120 | ass:move_to(tonumber(v["px"]), tonumber(0)) 121 | ass:line_to(tonumber(v["px"]), tonumber(osd_h)) 122 | end 123 | if v["t"] == "hline" then 124 | ass:move_to(tonumber(0), tonumber(v["py"])) 125 | ass:line_to(tonumber(osd_w), tonumber(v["py"])) 126 | end 127 | 128 | ass:draw_stop() 129 | ass:pos(0,0) 130 | 131 | end 132 | 133 | 134 | registrationAss = ass.text; 135 | draw_merged_ssa() 136 | end 137 | 138 | local function ass_escape(str) 139 | str = str:gsub('\\', '\\\239\187\191') 140 | str = str:gsub('{', '\\{') 141 | str = str:gsub('}', '\\}') 142 | -- Precede newlines with a ZWNBSP to prevent ASS's weird collapsing of 143 | -- consecutive newlines 144 | str = str:gsub('\n', '\239\187\191\\N') 145 | -- Turn leading spaces into hard spaces to prevent ASS from stripping them 146 | str = str:gsub('\\N ', '\\N\\h') 147 | str = str:gsub('^ ', '\\h') 148 | return str 149 | end 150 | 151 | 152 | 153 | function screenspacetools_rect(p1x,p1y,p2x,p2y,dim,fill,border,width,visible) 154 | 155 | local osd_w, osd_h = mp.get_property("osd-width"), mp.get_property("osd-height") 156 | 157 | if visible == "outer" then 158 | ass = assdraw.ass_new() 159 | ass:new_event() 160 | ass:pos(0, 0) 161 | ass:draw_start() 162 | ass:pos(0, 0) 163 | 164 | ass:append(ass_set_color(1, fill)) 165 | ass:append(ass_set_color(3, border)) 166 | ass:append("{\\bord1}") 167 | 168 | ass:rect_cw(tonumber(p1x), tonumber(p1y), tonumber(p2x), tonumber(p2y)) 169 | 170 | ass:pos(0, 0) 171 | ass:draw_stop() 172 | 173 | mp.set_osd_ass(osd_w, osd_h, ass.text) 174 | end 175 | if visible == "inner" then 176 | ass = assdraw.ass_new() 177 | ass:new_event() 178 | ass:draw_start() 179 | ass:pos(0, 0) 180 | 181 | ass:append(ass_set_color(1, fill)) 182 | ass:append(ass_set_color(3, "00000000")) 183 | 184 | local l = math.min(tonumber(p1x), tonumber(p2x)) 185 | local r = math.max(tonumber(p1x), tonumber(p2x)) 186 | local u = math.min(tonumber(p1y), tonumber(p2y)) 187 | local d = math.max(tonumber(p1y), tonumber(p2y)) 188 | 189 | local midy = tonumber((tonumber(p1x) + tonumber(p2x))/2) 190 | local midx = tonumber((tonumber(p1y) + tonumber(p2y))/2) 191 | 192 | local thirdx = tonumber(math.abs((tonumber(p1x) - tonumber(p2x)))/3) 193 | local thirdy = tonumber(math.abs((tonumber(p1y) - tonumber(p2y)))/3) 194 | 195 | 196 | ass:rect_cw(0, 0, l, osd_h) 197 | ass:rect_cw(r, 0, osd_w, osd_h) 198 | ass:rect_cw(l, 0, r, u) 199 | ass:rect_cw(l, d, r, osd_h) 200 | 201 | ass:draw_stop() 202 | 203 | -- Draw border around selected region 204 | 205 | 206 | 207 | ass:new_event() 208 | ass:draw_start() 209 | ass:pos(0, 0) 210 | 211 | ass:append(ass_set_color(1, "00000000")) 212 | ass:append(ass_set_color(3, "9F9F9FDD")) 213 | ass:append("{\\bord1}") 214 | 215 | ass:rect_cw(tonumber(l+thirdx), tonumber(u), tonumber(r-thirdx), tonumber(d)) 216 | 217 | 218 | ass:draw_stop() 219 | 220 | 221 | ass:new_event() 222 | ass:draw_start() 223 | ass:pos(0, 0) 224 | 225 | ass:append(ass_set_color(1, "00000000")) 226 | ass:append(ass_set_color(3, "9F9F9FDD")) 227 | ass:append("{\\bord1}") 228 | 229 | ass:rect_cw(tonumber(l), tonumber(u+thirdy), tonumber(r), tonumber(d-thirdy)) 230 | 231 | 232 | ass:draw_stop() 233 | 234 | 235 | ass:new_event() 236 | ass:draw_start() 237 | ass:pos(0, 0) 238 | 239 | ass:append(ass_set_color(1, "00000000")) 240 | ass:append(ass_set_color(3, "9F9F9FAA")) 241 | ass:append("{\\bord1}") 242 | ass:rect_cw(tonumber(p1x), tonumber(p1y), tonumber(midy), tonumber(midx)) 243 | ass:draw_stop() 244 | 245 | 246 | ass:new_event() 247 | ass:draw_start() 248 | ass:pos(0, 0) 249 | 250 | ass:append(ass_set_color(1, "00000000")) 251 | ass:append(ass_set_color(3, "9F9F9FAA")) 252 | ass:append("{\\bord1}") 253 | ass:rect_cw(tonumber(midy), tonumber(midx), tonumber(p2x), tonumber(p2y)) 254 | ass:draw_stop() 255 | 256 | 257 | ass:new_event() 258 | ass:draw_start() 259 | ass:pos(0, 0) 260 | 261 | ass:append(ass_set_color(1, "00000000")) 262 | ass:append(ass_set_color(3, "FF0000FF")) 263 | ass:append("{\\bord1}") 264 | ass:rect_cw(tonumber(midy)-15, tonumber(midx)-15, tonumber(midy)+15, tonumber(midx)+15) 265 | ass:draw_stop() 266 | 267 | 268 | ass:new_event() 269 | ass:draw_start() 270 | ass:pos(0, 0) 271 | 272 | ass:append(ass_set_color(1, "00000000")) 273 | ass:append(ass_set_color(3, border)) 274 | ass:append("{\\bord1}") 275 | 276 | ass:rect_cw(tonumber(p1x), tonumber(p1y), tonumber(p2x), tonumber(p2y)) 277 | ass:pos(0, 0) 278 | 279 | ass:draw_stop() 280 | 281 | ass:new_event() 282 | 283 | 284 | ass:append(ass_set_color(1, border)) 285 | ass:append(ass_set_color(3, border)) 286 | 287 | ass:append("{\\fs18}") 288 | ass:append("{\\bord0}") 289 | 290 | ass:pos(tonumber(p1x), tonumber(p2y)) 291 | 292 | ass:append( ass_escape( dim )) 293 | 294 | ass:pos(0, 0) 295 | 296 | cropAss = ass.text; 297 | draw_merged_ssa() 298 | end 299 | end 300 | 301 | function draw_merged_ssa() 302 | local osd_w, osd_h = mp.get_property("osd-width"), mp.get_property("osd-height") 303 | mp.set_osd_ass(osd_w, osd_h, cropAss .. registrationAss .. vectorAss .. sketchAss .. mouseAss ) 304 | end 305 | 306 | 307 | function screenspacetools_mouse_cross(x,y) 308 | local osd_w, osd_h = mp.get_property("osd-width"), mp.get_property("osd-height") 309 | if x==0 and y==0 then 310 | mouseAss=""; 311 | else 312 | ass = assdraw.ass_new() 313 | 314 | ass:new_event() 315 | ass:pos(0, 0) 316 | ass:draw_start() 317 | ass:pos(0, 0) 318 | 319 | ass:append(ass_set_color(1, "00000000")) 320 | ass:append(ass_set_color(3, "0000ffff")) 321 | ass:append("{\\bord0.2}") 322 | 323 | ass:move_to(tonumber(0), tonumber(y)) 324 | ass:line_to(tonumber(osd_w), tonumber(y)) 325 | ass:move_to(tonumber(x), tonumber(0)) 326 | ass:line_to(tonumber(x), tonumber(osd_h)) 327 | 328 | ass:pos(0,0) 329 | ass:draw_stop() 330 | 331 | mouseAss = ass.text 332 | end 333 | draw_merged_ssa() 334 | end 335 | 336 | local function eval(s) 337 | return assert(load(s))() 338 | end 339 | 340 | local function str2obj(s) 341 | return eval("return " .. s) 342 | end 343 | 344 | 345 | function screenspacetools_sketch(lines) 346 | local osd_w, osd_h = mp.get_property("osd-width"), mp.get_property("osd-height") 347 | 348 | ass = assdraw.ass_new() 349 | 350 | local linesobj = str2obj(lines) 351 | 352 | ass:pos(0,0) 353 | ass:new_event() 354 | ass:draw_start() 355 | ass:pos(0,0) 356 | 357 | ass:append(ass_set_color(1, "00000000")) 358 | ass:append(ass_set_color(3, "0000ffff")) 359 | ass:append("{\\bord2}") 360 | 361 | for k, v in pairs(linesobj) do 362 | local x1 = v[1]; 363 | local y1 = v[2]; 364 | local x2 = v[3]; 365 | local y2 = v[4]; 366 | local style = v[5]; 367 | 368 | 369 | ass:move_to(tonumber(x1), tonumber(y1)) 370 | ass:line_to(tonumber(x2), tonumber(y2)) 371 | end 372 | 373 | 374 | ass:pos(0,0) 375 | ass:draw_stop() 376 | 377 | sketchAss = ass.text 378 | draw_merged_ssa() 379 | end 380 | 381 | mp.register_script_message("screenspacetools_rect", screenspacetools_rect) 382 | mp.register_script_message("screenspacetools_clear", screenspacetools_clear) 383 | mp.register_script_message("screenspacetools_regMark", screenspacetools_regMark) 384 | mp.register_script_message("screenspacetools_drawVector", screenspacetools_drawVector) 385 | mp.register_script_message("screenspacetools_mouse_cross", screenspacetools_mouse_cross) 386 | mp.register_script_message("screenspacetools_sketch", screenspacetools_sketch) 387 | 388 | 389 | -------------------------------------------------------------------------------- /src/subtitleCutter.py: -------------------------------------------------------------------------------- 1 | 2 | from itertools import groupby 3 | from datetime import datetime 4 | 5 | def trimSRTfile(infilename,outfilename,sliceStart,sliceEnd): 6 | 7 | n=0 8 | tszero = datetime.strptime('00:00:00,000', '%H:%M:%S,%f') 9 | 10 | with open(outfilename,'w') as outfile: 11 | outfile.write('') 12 | for key,grp in groupby(open(infilename,'rb').readlines(),key=lambda x:x.strip()!=b''): 13 | try: 14 | if key: 15 | lstgrp = [x.decode('utf8',errors="ignore") for x in list(grp)] 16 | 17 | number,timestamp,*lines = lstgrp 18 | print(number,timestamp) 19 | 20 | tsstart,tsend = timestamp.split(' --> ') 21 | 22 | tsstart = tsstart.strip() 23 | tsend = tsend.strip() 24 | 25 | tsstart = datetime.strptime(tsstart, '%H:%M:%S,%f') 26 | tsend = datetime.strptime(tsend, '%H:%M:%S,%f') 27 | 28 | 29 | tsstart = (tsstart-tszero).total_seconds() 30 | tsend = (tsend-tszero).total_seconds() 31 | 32 | if not (tsendsliceEnd): 33 | n+=1 34 | tsstart = max(tsstart,sliceStart)-sliceStart 35 | tsend = min(tsend,sliceEnd)-sliceStart 36 | 37 | if tsstart==0: 38 | tsstart+=0.1 39 | 40 | tsstart_str = datetime.strftime(datetime.utcfromtimestamp(tsstart),'%H:%M:%S,%f') 41 | startP1,startP2 = tsstart_str.split(',') 42 | tsstart_str = startP1 + ',' + str(int(int(startP2)/1000)).zfill(3) 43 | 44 | tsend_str = datetime.strftime(datetime.utcfromtimestamp(tsend),'%H:%M:%S,%f') 45 | startP2,endP2 = tsend_str.split(',') 46 | tsend_str = startP2 + ',' + str(int(int(endP2)/1000)).zfill(3) 47 | 48 | 49 | timeString = "{}\n{} --> {}\n{}\n".format(n, 50 | tsstart_str, 51 | tsend_str, 52 | '\n'.join(lines) 53 | ) 54 | 55 | outfile.write(timeString) 56 | except Exception as e: 57 | print(e) 58 | if n == 0: 59 | outfile.write("1\n00:00:00,000 --> 00:00:00,000\n\n") -------------------------------------------------------------------------------- /src/videoClipSelectionFrameUI.py: -------------------------------------------------------------------------------- 1 | 2 | import tkinter as tk 3 | import tkinter.ttk as ttk 4 | 5 | import datetime 6 | import threading 7 | from math import floor 8 | import time 9 | import logging 10 | from threading import Lock 11 | 12 | 13 | 14 | class VideoClipSelectionFrameUI(ttk.Frame): 15 | 16 | def __init__(self, master, controller, globalOptions={}, *args, **kwargs): 17 | ttk.Frame.__init__(self, master) 18 | self.controller = controller 19 | self.globalOptions=globalOptions 20 | 21 | self.clip_canvas = tk.Canvas(self,width=200, height=200, bg='#1E1E1E',borderwidth=0,border=0,relief='flat',highlightthickness=0) 22 | self.clip_canvas.grid(row=1,column=0,sticky="nesw") 23 | self.grid_rowconfigure(1, weight=1) 24 | self.grid_columnconfigure(0, weight=1) 25 | 26 | self.uiDirty=True 27 | 28 | if __name__ == '__main__': 29 | import webmGenerator -------------------------------------------------------------------------------- /src/videoManager.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | class VideoManager: 4 | 5 | def __init__(self,globalOptions={}): 6 | self.subclips = {} 7 | self.labels = {} 8 | self.seqGroups = {} 9 | self.interestMarks = {} 10 | self.subClipCounter=0 11 | self.globalOptions=globalOptions 12 | self.subclipChangeCallbacks=[] 13 | self.lastgroup='0' 14 | 15 | def updateLabelForClip(self,filename,rid,label): 16 | self.labels[rid] = label 17 | print(self.labels) 18 | 19 | def getLabelForClip(self,filename,rid): 20 | print(self.labels) 21 | return self.labels.get(rid,'') 22 | 23 | def updateSeqGroupForClip(self,filename,rid,groupId): 24 | self.seqGroups[rid] = groupId 25 | self.lastgroup = groupId 26 | print(self.seqGroups) 27 | 28 | def getSeqGroupForClip(self,filename,rid): 29 | return self.seqGroups.get(rid,'0') 30 | 31 | def addSubclipChangeCallback(self,callback): 32 | if callback not in self.subclipChangeCallbacks: 33 | self.subclipChangeCallbacks.append(callback) 34 | 35 | def updateCallbacks(self,rid=None,pos=None,action='UPDATE'): 36 | for callback in self.subclipChangeCallbacks: 37 | callback(rid=rid,pos=pos,action=action) 38 | 39 | def getStateForSave(self): 40 | return {'subclips':self.subclips.copy(), 41 | 'interestMarks':{}, 42 | 'subClipCounter':self.subClipCounter, 43 | 'labels':self.labels, 44 | } 45 | 46 | def loadStateFromSave(self,data): 47 | self.subclips = data.get('subclips',{}) 48 | self.interestMarks.clear() 49 | self.subClipCounter = data['subClipCounter'] 50 | self.labels = data.get('labels',{}) 51 | 52 | def reset(self): 53 | for filename,clips in self.subclips.items(): 54 | for rid,(s,e) in clips.items(): 55 | self.updateCallbacks(rid=rid,pos='s',action='REMOVE') 56 | self.subclips.clear() 57 | self.interestMarks.clear() 58 | self.labels.clear() 59 | 60 | def addNewInterestMark(self,filename,point,kind='manual'): 61 | self.interestMarks.setdefault(filename,set()).add((point,kind)) 62 | 63 | def getInterestMarks(self,filename): 64 | return list(self.interestMarks.get(filename,[])) 65 | 66 | def clearallSubclipsOnFile(self,filename): 67 | if filename in self.subclips: 68 | for rid,(s,e) in self.subclips.get(filename,{}).items(): 69 | self.updateCallbacks(rid=rid,pos='s',action='REMOVE') 70 | self.subclips[filename].clear() 71 | 72 | def clearallInterestMarksOnFile(self,filename): 73 | if filename in self.subclips: 74 | self.interestMarks[filename]=set() 75 | 76 | def clearallSubclips(self): 77 | for filename,clips in self.subclips.items(): 78 | for rid,(s,e) in clips.items(): 79 | self.updateCallbacks(rid=rid,pos='s',action='REMOVE') 80 | self.subclips.clear() 81 | 82 | def getAllClips(self): 83 | result=[] 84 | for filename,clips in self.subclips.items(): 85 | for rid,(s,e) in clips.items(): 86 | result.append( (filename,rid,s,e) ) 87 | return result 88 | 89 | def removeVideo(self,filename): 90 | if filename in self.subclips: 91 | for rid,(s,e) in self.subclips.get(filename,{}).items(): 92 | self.updateCallbacks(rid=rid,pos='s',action='REMOVE') 93 | del self.subclips[filename] 94 | 95 | def registerNewSubclip(self,filename,start,end): 96 | self.subClipCounter+=1 97 | start,end = sorted([start,end]) 98 | self.subclips.setdefault(filename,{})[self.subClipCounter]=[start,end] 99 | self.updateCallbacks(rid=self.subClipCounter,pos='s',action='NEW') 100 | self.updateSeqGroupForClip(filename,self.subClipCounter,self.lastgroup) 101 | return self.subClipCounter 102 | 103 | def getSurroundingInterestMarks(self,filename,point): 104 | s,e = point,point 105 | 106 | try: 107 | s = max([x[0] for x in self.interestMarks.get(filename) if x[0]<=s]) 108 | except Exception as ex: 109 | logging.error("expandSublcipToInterestMarks",exc_info=ex) 110 | 111 | try: 112 | e = min([x[0] for x in self.interestMarks.get(filename) if x[0]>=e]) 113 | except Exception as ex: 114 | logging.error("expandSublcipToInterestMarks",exc_info=ex) 115 | 116 | return s,e 117 | 118 | def expandSublcipToInterestMarks(self,filename,point): 119 | targetRid = None 120 | newStart = None 121 | newEnd = None 122 | print('expand to interestMarks') 123 | for rid,(s,e) in self.subclips.get(filename,{}).items(): 124 | if s=e]) 135 | print(newEnd) 136 | except Exception as e: 137 | logging.error("expandSublcipToInterestMarks",exc_info=e) 138 | break 139 | 140 | print(filename,targetRid,newStart,newEnd) 141 | 142 | if targetRid is not None: 143 | self.updateDetailsForRangeId(filename,targetRid,newStart,newEnd) 144 | 145 | def cloneSubclip(self,filename,point): 146 | clipsToClone=set() 147 | for rid,(s,e) in self.subclips.get(filename,{}).items(): 148 | if s= sampleWindowLength: 104 | start = (n-sampleWindowLength)*(frame_duration/1000) 105 | end = (n)*(frame_duration/1000) 106 | mean = sum(sampqueue)/len(sampqueue) 107 | 108 | meanzcr = (sum(zcrqueue)/len(zcrqueue))*100 109 | zrcPass = meanzcr==0 or ((zcrMin == -1 or meanzcr>=zcrMin) and (zcrMax == -1 or meanzcr<=zcrMax)) 110 | 111 | if positiveMode: 112 | if zrcPass and mean >= condidenceStart and startTS is None: 113 | startTS=start 114 | endTS=end 115 | elif zrcPass and mean >= condidenceEnd and startTS is not None and endTS is not None and endTS >= start-bridgeDistance: 116 | endTS=end 117 | elif mean < condidenceEnd and startTS is not None and endTS is not None and endTS <= start-bridgeDistance: 118 | if endTS-startTS>=minimimDuration: 119 | callback(filename,startTS,timestampEnd=endTS,kind='Cut') 120 | startTS=None 121 | endTS=None 122 | else: 123 | if zrcPass and mean <= condidenceStart and startTS is None and zrcPass: 124 | startTS=start 125 | endTS=end 126 | elif zrcPass and mean <= condidenceEnd and startTS is not None and endTS is not None and endTS >= start-bridgeDistance: 127 | endTS=end 128 | elif mean > condidenceEnd and startTS is not None and endTS is not None and endTS <= start-bridgeDistance: 129 | if endTS-startTS>=minimimDuration: 130 | callback(filename,startTS,timestampEnd=endTS,kind='Cut') 131 | startTS=None 132 | endTS=None 133 | 134 | if startTS is not None and endTS is not None: 135 | if endTS-startTS>=minimimDuration: 136 | callback(filename,startTS,timestampEnd=endTS,kind='Cut') 137 | startTS=None 138 | endTS=None 139 | 140 | except Exception as e: 141 | print(e) 142 | 143 | self.globalStatusCallback('Voice activity scan complete',1) 144 | self.voiceDetectRequestQueue.task_done() 145 | 146 | self.voiceWorkerThread = threading.Thread(target=processFaceRequests,daemon=True) 147 | self.voiceWorkerThread.start() 148 | 149 | def scanForVoiceActivity(self,fileName,totalDuration,callback,sampleLength,aggresiveness,windowLength,minimimDuration,bridgeDistance,condidenceStart,condidenceEnd,zcrMin,zcrMax): 150 | print(fileName,totalDuration,callback,sampleLength,aggresiveness,windowLength,minimimDuration,bridgeDistance,condidenceStart,condidenceEnd,zcrMin,zcrMax) 151 | self.voiceDetectRequestQueue.put((fileName,totalDuration,callback,sampleLength,aggresiveness,windowLength,minimimDuration,bridgeDistance,condidenceStart,condidenceEnd,zcrMin,zcrMax)) 152 | -------------------------------------------------------------------------------- /src/vrscript.lua: -------------------------------------------------------------------------------- 1 | 2 | local assdraw = require("mp.assdraw") 3 | 4 | local dragging = false 5 | local filterIsOn = false 6 | local updateAwaiting = false 7 | 8 | local startRecordOnNextLoop = false 9 | local recording = false 10 | local recordingComplete = false 11 | local firstPass = true 12 | 13 | local yaw = 0.0 14 | local last_yaw = 0.0 15 | local init_yaw = 0.0 16 | 17 | local pitch = 0.0 18 | local last_pitch = 0.0 19 | local init_pitch = 0.0 20 | 21 | local roll = 0.0 22 | local last_roll = 0.0 23 | local init_roll = 0.0 24 | 25 | local smoothMouse=true 26 | local smoothFactor = 5 27 | 28 | local inputProjection = "hequirect" 29 | local outputProjection = "flat" 30 | 31 | 32 | local idfov=180.0 33 | local dfov=110.0 34 | local last_dfov = 110.0 35 | local init_dfov = 0.0 36 | 37 | local fw=100 38 | local fh=100 39 | 40 | local scaling = 'linear' 41 | 42 | local in_stereo = 'sbs' 43 | 44 | local h_flip = '0' 45 | local in_flip = '' 46 | 47 | local interp = 'cubic' 48 | 49 | 50 | local updateComplete = function() 51 | updateAwaiting = false 52 | end 53 | 54 | local writeHeadPositionChange = function() 55 | 56 | local newTimePos = mp.get_property("time-pos") 57 | 58 | if pitch ~= last_pitch then 59 | mp.command(string.format("script-message vrscript setValue pitch %.3f %.3f",newTimePos,pitch)) 60 | if firstPass then 61 | mp.command(string.format("script-message vrscript setInitValue pitch %.3f %.3f",newTimePos,pitch)) 62 | end 63 | end 64 | last_pitch=pitch 65 | 66 | if yaw ~= last_yaw then 67 | mp.command(string.format("script-message vrscript setValue yaw %.3f %.3f",newTimePos,yaw)) 68 | if firstPass then 69 | mp.command(string.format("script-message vrscript setInitValue yaw %.3f %.3f",newTimePos,yaw)) 70 | end 71 | end 72 | last_yaw=yaw 73 | 74 | if dfov ~= last_dfov then 75 | mp.command(string.format("script-message vrscript setValue d_fov %.3f %.3f",newTimePos,dfov)) 76 | if firstPass then 77 | mp.command(string.format("script-message vrscript setInitValue d_fov %.3f %.3f",newTimePos,dfov)) 78 | end 79 | end 80 | last_dfov=dfov 81 | 82 | if roll ~= last_roll then 83 | mp.command(string.format("script-message vrscript setValue roll %.3f %.3f",newTimePos,roll)) 84 | if firstPass then 85 | mp.command(string.format("script-message vrscript setInitValue roll %.3f %.3f",newTimePos,roll)) 86 | end 87 | end 88 | last_roll=roll 89 | 90 | firstPass=false 91 | end 92 | 93 | local updateFilters = function () 94 | if not filterIsOn then 95 | mp.command_native_async({"no-osd", "vf", "add", string.format("@vrrev:%sv360=%s:%s:reset_rot=1:in_stereo=%s:out_stereo=2d:id_fov=%s:d_fov=%.3f:yaw=%.3f:pitch=%s:roll=%.3f:w=%.3f:h=%.3f:h_flip=%s:interp=%s",in_flip,inputProjection,outputProjection,in_stereo,idfov,dfov,yaw,pitch,roll,fw,fh,h_flip,scaling)}, updateComplete) 96 | filterIsOn=true 97 | elseif not updateAwaiting then 98 | updateAwaiting=true 99 | mp.command_native_async({"no-osd", "vf", "set", string.format("@vrrev:%sv360=%s:%s:reset_rot=1:in_stereo=%s:out_stereo=2d:id_fov=%s:d_fov=%.3f:yaw=%.3f:pitch=%s:roll=%.3f:w=%.3f:h=%.3f:h_flip=%s:interp=%s",in_flip,inputProjection,outputProjection,in_stereo,idfov,dfov,yaw,pitch,roll,fw,fh,h_flip,scaling)}, updateComplete) 100 | end 101 | if recording then 102 | writeHeadPositionChange() 103 | end 104 | end 105 | 106 | local mouse_move_cb = function () 107 | if dragging then 108 | 109 | local MousePosx, MousePosy = mp.get_mouse_pos() 110 | local osd_w, osd_h = mp.get_property("osd-width"), mp.get_property("osd-height") 111 | 112 | 113 | local yawpc = ((MousePosx/osd_w)-0.5)*180 114 | local pitchpc = -((MousePosy/osd_h)-0.5)*180 115 | 116 | local updateCrop = false 117 | 118 | 119 | if smoothMouse then 120 | if yaw ~= yawpc and math.abs(yaw-yawpc)<0.1 then 121 | yaw = yawpc 122 | updateCrop=true 123 | yaw = math.max(-180,math.min(180,yaw)) 124 | elseif yaw ~= yawpc then 125 | yaw = (yawpc+(yaw*smoothFactor))/(smoothFactor+1) 126 | yaw = math.max(-180,math.min(180,yaw)) 127 | updateCrop=true 128 | end 129 | 130 | if pitch ~= pitchpc and math.abs(pitch-pitchpc)<0.1 then 131 | pitch = pitchpc 132 | pitch = math.max(-180,math.min(180,pitch)) 133 | updateCrop=true 134 | elseif pitch ~= pitchpc then 135 | pitch = (pitchpc+(pitch*smoothFactor))/(smoothFactor+1) 136 | pitch = math.max(-180,math.min(180,pitch)) 137 | updateCrop=true 138 | end 139 | else 140 | if yaw ~= yawpc then 141 | yaw = yawpc 142 | yaw = math.max(-180,math.min(180,yaw)) 143 | updateCrop=true 144 | end 145 | if pitch ~= pitchpc then 146 | pitch = pitchpc 147 | pitch = math.max(-180,math.min(180,pitch)) 148 | updateCrop=true 149 | end 150 | 151 | end 152 | 153 | if updateCrop then 154 | updateFilters() 155 | end 156 | 157 | end 158 | end 159 | 160 | local mouse_btn0_cb = function () 161 | dragging = not dragging 162 | if dragging then 163 | mp.set_property("cursor-autohide", "always") 164 | else 165 | mp.set_property("cursor-autohide", "no") 166 | end 167 | end 168 | 169 | local reset_and_record = function() 170 | mp.set_property("time-pos", mp.get_property("ab-loop-a")) 171 | mp.set_property("pause", "no") 172 | startRecordOnNextLoop = true 173 | recording = false 174 | mp.command(string.format("script-message vrscript resetRecording None None None")) 175 | end 176 | 177 | 178 | 179 | function playback_resetart_cb(event) 180 | mp.command(string.format("script-message vrscript loopRestart None None None")) 181 | recordingComplete = false 182 | if startRecordOnNextLoop then 183 | recording = true 184 | firstPass = true 185 | recordingComplete = false 186 | else 187 | if recording then 188 | recordingComplete = true 189 | end 190 | recording = false 191 | end 192 | startRecordOnNextLoop=false 193 | 194 | end 195 | 196 | -- Wrapper that converts RRGGBB / RRGGBBAA to ASS format 197 | local ass_set_color = function (idx, color) 198 | assert(color:len() == 8 or color:len() == 6) 199 | local ass = "" 200 | 201 | -- Set alpha value (if present) 202 | if color:len() == 8 then 203 | local alpha = 0xff - tonumber(color:sub(7, 8), 16) 204 | ass = ass .. string.format("\\%da&H%X&", idx, alpha) 205 | end 206 | 207 | -- Swizzle RGB to BGR and build ASS string 208 | color = color:sub(5, 6) .. color:sub(3, 4) .. color:sub(1, 2) 209 | return "{" .. ass .. string.format("\\%dc&H%s&", idx, color) .. "}" 210 | end 211 | 212 | function display_status() 213 | local ass = assdraw.ass_new() 214 | local la = mp.get_property("ab-loop-a") 215 | local lb = mp.get_property("ab-loop-b") 216 | local tp = mp.get_property("time-pos") 217 | 218 | local playbackpc = 0.0 219 | 220 | if tp ~= nil and tp ~= '' then 221 | playbackpc = ((tp-la)/(lb-la))*100 222 | end 223 | 224 | ass:new_event() 225 | ass:pos(5, 5) 226 | ass:append("{\\fnUbuntu\\fs30\\b1\\bord1}") 227 | if recording then 228 | ass:append("{\\c&H00FF00&}- Recording Head Motion -{\\c&HFFFFFF&}\\N") 229 | else 230 | ass:append("{\\c&H0000FF&}Not Recording Head Motion{\\c&HFFFFFF&}\\N") 231 | end 232 | 233 | ass:append(string.format("P=%.3f Y=%.3f Z=%.3f LoopPercent=%.3f%%\\N",pitch,yaw,dfov,playbackpc)) 234 | 235 | if smoothMouse then 236 | ass:append(string.format("Mouse smoothing on at factor %.1f press S cycle.\\N",smoothFactor)) 237 | else 238 | ass:append("Mouse smoothing off press S cycle.\\N") 239 | end 240 | ass:append("Press R to restart loop and record motions.\\N") 241 | 242 | if recordingComplete then 243 | ass:append("{\\c&H00FF00&}Complete motion recording saved.\\N") 244 | else 245 | ass:append("{\\c&H0000FF&}Incomplete motion recoding.{\\c&HFFFFFF&}\\N") 246 | end 247 | 248 | ass:append("Press Q to quit.{\\c&HFFFFFF&}") 249 | 250 | 251 | ass:new_event() 252 | ass:draw_start() 253 | ass:pos(0, 0) 254 | 255 | ass:append(ass_set_color(1, "000000AA")) 256 | ass:append(ass_set_color(3, "000000ff")) 257 | ass:append("{\\bord0}") 258 | 259 | local osd_w, osd_h = mp.get_property("osd-width"), mp.get_property("osd-height") 260 | 261 | ass:rect_cw(0, osd_h-10, osd_w, osd_h) 262 | 263 | 264 | ass:draw_stop() 265 | 266 | ass:new_event() 267 | ass:draw_start() 268 | ass:pos(0, 0) 269 | 270 | ass:append(ass_set_color(1, "0000ffAA")) 271 | ass:append(ass_set_color(3, "000000ff")) 272 | ass:append("{\\bord0}") 273 | 274 | ass:rect_cw(0, osd_h-10, osd_w*(playbackpc/100), osd_h) 275 | 276 | ass:draw_stop() 277 | mp.set_osd_ass(osd_w, osd_h, ass.text) 278 | end 279 | 280 | local increment_zoom = function (inc) 281 | dfov = dfov+inc 282 | dfov = math.max(math.min(150, dfov), 30) 283 | updateFilters() 284 | end 285 | 286 | 287 | local increment_roll = function (inc) 288 | roll = roll+inc 289 | roll = math.max(math.min(180, roll), -180) 290 | updateFilters() 291 | end 292 | 293 | local atexit = function() 294 | mp.command("script-message vrscript exit None None None") 295 | mp.command("stop") 296 | mp.command("quit") 297 | end 298 | 299 | local initialiseValues = function(init_in_proj,init_out_proj,init_in_trans,init_out_trans,init_h_flip,init_ih_flip,init_iv_flip,init_in_stereo,init_out_stereo,init_w,init_h,init_yaw,init_pitch,init_roll,init_d_fov,init_id_fov,init_interp) 300 | 301 | inputProjection=init_in_proj 302 | outputProjection=init_out_proj 303 | in_stereo=init_in_stereo 304 | h_flip=init_ih_flip 305 | fw=init_w 306 | fh=init_h 307 | scaling=init_interp 308 | idfov=init_id_fov 309 | dfov=init_d_fov 310 | 311 | updateFilters() 312 | end 313 | 314 | local incrementSmoothing = function() 315 | 316 | if smoothFactor == 1 then 317 | smoothMouse=true 318 | smoothFactor=5 319 | elseif smoothFactor < 25 then 320 | smoothMouse=true 321 | smoothFactor = smoothFactor+5 322 | elseif smoothFactor == 25 then 323 | smoothMouse=false 324 | smoothFactor=1 325 | end 326 | 327 | 328 | end 329 | 330 | mp.register_script_message("vrscript_initialiseValues", initialiseValues) 331 | 332 | mp.register_event("playback-restart", playback_resetart_cb) 333 | 334 | mp.add_forced_key_binding('space', "toggle_vr360_pause", function() mp.set_property("pause", "no") end ) 335 | mp.add_forced_key_binding('q', "toggle_vr360_quit", atexit ) 336 | mp.add_forced_key_binding('r', "toggle_vr360_resetandRecord", reset_and_record ) 337 | mp.add_forced_key_binding('s', "toggle_vr360_smoothing", incrementSmoothing ) 338 | mp.add_forced_key_binding('mouse_btn0', "grab_mouse", mouse_btn0_cb ) 339 | mp.add_forced_key_binding('mouse_move', "move_mouse", mouse_move_cb ) 340 | 341 | 342 | mp.add_forced_key_binding('WHEEL_DOWN', "move_mouse_md", function() increment_zoom(1) end ) 343 | mp.add_forced_key_binding('WHEEL_UP', "move_mouse_mu", function() increment_zoom(-1) end ) 344 | 345 | mp.add_forced_key_binding('a', "roll_decrease", function() increment_roll(-1) end ) 346 | mp.add_forced_key_binding('d', "roll_increase", function() increment_roll(1) end ) 347 | 348 | mp.set_property("fullscreen", "yes") 349 | 350 | updateFilters() 351 | 352 | mp.add_periodic_timer(0.1, display_status) 353 | -------------------------------------------------------------------------------- /src/youtubeDLService.py: -------------------------------------------------------------------------------- 1 | 2 | import subprocess as sp 3 | import os 4 | import threading 5 | from queue import Queue,Empty,Full 6 | import traceback 7 | import signal 8 | import logging 9 | import ctypes 10 | 11 | 12 | class YTDLService(): 13 | 14 | def __init__(self,globalStatusCallback=print,globalOptions={}): 15 | self.globalStatusCallback = globalStatusCallback 16 | self.downloadRequestQueue = Queue() 17 | self.cancelEvent = threading.Event() 18 | self.splitEvent = threading.Event() 19 | self.pushPreview = False 20 | self.globalOptions=globalOptions 21 | 22 | 23 | self.inputFrameQueue = Queue(1) 24 | self.resultFrameQueue = Queue(1) 25 | 26 | def frameWorkerthread(): 27 | while 1: 28 | url = self.inputFrameQueue.get() 29 | frameCapProc = sp.Popen(["ffmpeg" 30 | ,"-loglevel", "quiet" 31 | ,"-noaccurate_seek" 32 | ,"-i", url 33 | ,"-to",'1' 34 | ,'-frames:v', '1' 35 | ,"-an" 36 | ,"-filter_complex", "scale=220:220:force_original_aspect_ratio=decrease:flags=area" 37 | ,'-f', 'rawvideo' 38 | ,"-pix_fmt", "rgb24" 39 | ,'-c:v', 'ppm' 40 | ,'-y' 41 | ,"-"],stdout=sp.PIPE,bufsize=10 ** 5) 42 | outs,errs = frameCapProc.communicate() 43 | print('outs',len(outs)) 44 | self.inputFrameQueue.task_done() 45 | self.resultFrameQueue.put(outs) 46 | 47 | 48 | self.framePreviewWorkerThread = threading.Thread(target=frameWorkerthread,daemon=True) 49 | self.framePreviewWorkerThread.start() 50 | 51 | def downloadFunc(): 52 | while 1: 53 | try: 54 | url,fileLimit,username,password,useCookies,browserCookies,qualitySort,code2Factor,callback,retrycount = self.downloadRequestQueue.get() 55 | 56 | self.cancelEvent.clear() 57 | self.splitEvent.clear() 58 | 59 | streamHasBeenCut = True 60 | 61 | if url == 'UPDATE': 62 | self.globalStatusCallback('yt-dlp upgrade',0.0) 63 | print(url) 64 | proc = sp.Popen(['yt-dlp','--update'],stdout=sp.PIPE) 65 | l = b'' 66 | while 1: 67 | c=proc.stdout.read(1) 68 | l+=c 69 | if len(c)==0: 70 | break 71 | if c in (b'\n',b'\r'): 72 | self.globalStatusCallback('yt-dlp upgrade {}'.format(l.decode('utf8',errors='ignore').strip()),0.0) 73 | self.globalStatusCallback('yt-dlp upgrade {}'.format(l.decode('utf8',errors='ignore').strip()),1.0) 74 | continue 75 | 76 | cutPassName=0 77 | while streamHasBeenCut: 78 | streamHasBeenCut=False 79 | cutPassName+=1 80 | 81 | tempPathname='tempDownloadedVideoFiles' 82 | os.path.exists(tempPathname) or os.mkdir(tempPathname) 83 | outfolder = os.path.join(tempPathname,self.globalOptions.get('downloadNameFormat','%(title)s-%(id)s_%(uploader,creator,channel)s_{passNumber}.%(ext)s').format(passNumber=cutPassName)) 84 | 85 | extraFlags=[] 86 | 87 | if len(username)>0 and len(password)>0: 88 | extraFlags.extend(['--username', username, '--password', password]) 89 | elif len(password)>0: 90 | extraFlags.extend(['--video-password', password]) 91 | 92 | if len(code2Factor)>0: 93 | extraFlags.extend(['--twofactor', code2Factor]) 94 | 95 | if fileLimit>0: 96 | extraFlags.extend(['--max-downloads',str(fileLimit)]) 97 | 98 | if useCookies and os.path.exists('cookies.txt'): 99 | extraFlags.extend(['--cookies','cookies.txt']) 100 | 101 | if len(browserCookies)>0: 102 | extraFlags.extend(['--cookies-from-browser',browserCookies]) 103 | 104 | if len(qualitySort)>0 and qualitySort.upper() != 'DEFAULT': 105 | extraFlags.extend(['-f',qualitySort]) 106 | 107 | print(extraFlags) 108 | 109 | if hasattr(os.sys, 'winver'): 110 | proc = sp.Popen(['yt-dlp','--ignore-errors','--keep-video','--restrict-filenames']+extraFlags+[url,'-o',outfolder,'--merge-output-format','mp4'],creationflags=sp.CREATE_NEW_PROCESS_GROUP,stderr=sp.STDOUT,stdout=sp.PIPE,bufsize=10 ** 5) 111 | else: 112 | proc = sp.Popen(['yt-dlp','--ignore-errors','--keep-video','--restrict-filenames']+extraFlags+[url,'-o',outfolder,'--merge-output-format','mp4'],stderr=sp.STDOUT,stdout=sp.PIPE,bufsize=10 ** 5) 113 | 114 | l = b'' 115 | self.globalStatusCallback('Download start {}'.format(url),0) 116 | logging.debug("Downloading {}".format(url)) 117 | finalName = b'' 118 | 119 | seenFiles = set() 120 | emittedFiles = set() 121 | timestamp='00:00:00' 122 | frameouts=None 123 | 124 | lastpicst=None 125 | 126 | try: 127 | self.resultFrameQueue.get_nowait() 128 | self.resultFrameQueue.task_done() 129 | except Empty: 130 | pass 131 | 132 | while 1: 133 | c=proc.stdout.read(1) 134 | 135 | 136 | lastStreamcutVal=streamHasBeenCut 137 | if self.splitEvent.is_set(): 138 | streamHasBeenCut=True 139 | self.splitEvent.clear() 140 | 141 | if self.cancelEvent.is_set() or (streamHasBeenCut and lastStreamcutVal!=streamHasBeenCut): 142 | try: 143 | if hasattr(os.sys, 'winver'): 144 | os.kill(proc.pid, signal.CTRL_BREAK_EVENT) 145 | else: 146 | proc.send_signal(signal.SIGTERM) 147 | except Exception as ex: 148 | print(ex) 149 | try: 150 | proc.kill() 151 | except Exception as ex: 152 | print(ex) 153 | 154 | self.cancelEvent.clear() 155 | print('CANCEL SENT AND CLEARED') 156 | if streamHasBeenCut: 157 | self.globalStatusCallback('Download streaming (splitting) {}'.format(finalName.decode('utf8',errors='ignore')),-1) 158 | else: 159 | self.globalStatusCallback('Download complete (cancelled) {}'.format(finalName.decode('utf8',errors='ignore')),1.0) 160 | 161 | l+=c 162 | if len(c)==0: 163 | break 164 | 165 | acceptedFinalFormats = ['.mp4'] 166 | 167 | if c in (b'\n',b'\r'): 168 | print(l) 169 | 170 | 171 | 172 | picst=None 173 | 174 | if self.pushPreview and l is not None and b"] Opening 'https:" in l and (b'.m3u8'.upper() not in l.upper()): 175 | picst = l 176 | picst = picst[picst.index(b"'https:")+1:] 177 | picst = picst[:picst.index(b"'")] 178 | print("currentTSStream:",picst) 179 | if picst == lastpicst: 180 | picst=None 181 | else: 182 | lastpicst = picst 183 | 184 | if picst is not None and self.pushPreview: 185 | try: 186 | self.inputFrameQueue.put_nowait(picst) 187 | picst=None 188 | except Full: 189 | pass 190 | 191 | try: 192 | frameouts = self.resultFrameQueue.get_nowait() 193 | self.resultFrameQueue.task_done() 194 | self.globalStatusCallback('Download streaming {} {}'.format(finalName.decode('utf8',errors='ignore'),timestamp), -1,progressPreview=frameouts) 195 | except Empty: 196 | pass 197 | 198 | if b'time=' in l and b'bitrate=' in l: 199 | timestamp = l.split(b'time=')[1].split(b'bitrate=')[0].strip().decode('utf8',errors='ignore') 200 | self.globalStatusCallback('Download streaming {} {}'.format(finalName.decode('utf8',errors='ignore'),timestamp), -1,progressPreview=frameouts) 201 | 202 | if b'[download] Destination:' in l: 203 | finalName = l.replace(b'[download] Destination: ',b'').strip() 204 | if finalName.endswith(b'.mp4'): 205 | seenFiles.add(finalName) 206 | if b'[ffmpeg] Merging formats into' in l: 207 | finalName = l.split(b'"')[-2].strip() 208 | if finalName.endswith(b'.mp4'): 209 | seenFiles.add(finalName) 210 | self.globalStatusCallback('Download complete {}'.format(finalName),1.0) 211 | logging.debug("Download complete {}".format(finalName)) 212 | if b'[Merger] Merging formats into' in l: 213 | finalName = l.split(b'"')[-2].strip() 214 | if finalName.endswith(b'.mp4'): 215 | seenFiles.add(finalName) 216 | self.globalStatusCallback('Download complete {}'.format(finalName),1.0) 217 | logging.debug("Download complete {}".format(finalName)) 218 | 219 | if b'[download]' in l and b' has already been downloaded and merged' in l: 220 | finalName = l.replace(b' has already been downloaded and merged',b'').replace(b'[download] ',b'').strip() 221 | if finalName.endswith(b'.mp4'): 222 | seenFiles.add(finalName) 223 | self.globalStatusCallback('Download already complete {}'.format(finalName),1.0) 224 | elif b'[download]' in l and b' has already been downloaded' in l: 225 | finalName = l.replace(b' has already been downloaded',b'').replace(b'[download] ',b'').strip() 226 | if finalName.endswith(b'.mp4'): 227 | seenFiles.add(finalName) 228 | self.globalStatusCallback('Download already complete {}'.format(finalName),1.0) 229 | 230 | 231 | if b"Deleting original file " in l: 232 | finalName = l.replace(b'Deleting original file',b'').replace(b'(pass -k to keep)',b'').strip() 233 | emittedFiles.add(finalName) 234 | 235 | 236 | if b'[download]' in l and b'%' in l: 237 | try: 238 | pc = b'0' 239 | for tc in l.split(b' '): 240 | if b'%' in tc: 241 | pc = tc.replace(b'%',b'') 242 | desc = l.replace(b'[download]',b'').strip().decode('utf8',errors='ignore') 243 | self.globalStatusCallback('Download progress {} {}'.format(url,desc),float(pc)/100) 244 | if int(float(pc)) == 100 and len(finalName)>0: 245 | self.globalStatusCallback('Download complete {}'.format(finalName),1.0) 246 | except Exception as e: 247 | print(e) 248 | traceback.print_exc() 249 | 250 | if finalName is not None: 251 | for seenfilename in seenFiles: 252 | if seenfilename not in emittedFiles and len(seenfilename)>0 and seenfilename != finalName: 253 | 254 | emitName = seenfilename.decode('utf8') 255 | if emitName.endswith('.mp4') and '.fhls' not in emitName: 256 | self.globalStatusCallback('Download complete {}'.format(emitName),1.0) 257 | if os.path.exists(emitName): 258 | callback(os.path.abspath(emitName)) 259 | emittedFiles.add(seenfilename) 260 | else: 261 | callback(os.path.abspath(emitName+'.part')) 262 | emittedFiles.add(seenfilename) 263 | 264 | l=b'' 265 | if len(seenFiles)>0: 266 | for seenfilename in seenFiles: 267 | if seenfilename not in emittedFiles and len(seenfilename)>0: 268 | emitName = seenfilename.decode('utf8') 269 | if emitName.endswith('.mp4') and '.fhls' not in emitName: 270 | self.globalStatusCallback('Download complete {}'.format(emitName),1.0) 271 | if os.path.exists(emitName): 272 | callback(os.path.abspath(emitName)) 273 | emittedFiles.add(seenfilename) 274 | else: 275 | callback(os.path.abspath(emitName+'.part')) 276 | emittedFiles.add(seenfilename) 277 | 278 | else: 279 | self.globalStatusCallback('Download failed {}'.format(url),1.0) 280 | except Exception as e: 281 | print(e) 282 | traceback.print_exc() 283 | self.globalStatusCallback('Download failed {}'.format(url),1.0) 284 | 285 | 286 | 287 | self.downloadWorkerThread = threading.Thread(target=downloadFunc,daemon=True) 288 | self.downloadWorkerThread.start() 289 | 290 | def togglePreview(self,toggleValue): 291 | self.pushPreview = toggleValue 292 | 293 | def loadUrl(self,url,fileLimit,username,password,useCookies,browserCookies,qualitySort,code2Factor,callback,retrycount=0): 294 | self.downloadRequestQueue.put((url,fileLimit,username,password,useCookies,browserCookies,qualitySort,code2Factor,callback,retrycount)) 295 | 296 | def update(self): 297 | self.downloadRequestQueue.put(('UPDATE',None,None,None,None,None,None,None,None,None)) 298 | 299 | def cancelCurrentYoutubeDl(self): 300 | self.cancelEvent.set() 301 | 302 | def splitStream(self): 303 | self.splitEvent.set() 304 | 305 | 306 | 307 | if __name__ == '__main__': 308 | import webmGenerator -------------------------------------------------------------------------------- /webmGenerator.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | try: 4 | import logging 5 | import sys 6 | import os 7 | import traceback 8 | 9 | print("Initial working directory", os.getcwd()) 10 | 11 | if getattr(sys, 'frozen', False): 12 | os.chdir(os.path.abspath(os.path.realpath(os.path.dirname(sys.executable)))) 13 | else: 14 | os.chdir(os.path.abspath(os.path.realpath(os.path.dirname(__file__)))) 15 | 16 | print("Current working directory", os.getcwd()) 17 | 18 | logging.basicConfig( 19 | level=logging.INFO, 20 | format="%(asctime)s [%(threadName)-12.12s] [%(levelname)-5.5s] %(message)s", 21 | handlers=[ 22 | logging.FileHandler("debug.log"), 23 | logging.StreamHandler() 24 | ] 25 | ) 26 | 27 | logging.info('Startup.') 28 | 29 | from src.webmGeneratorController import WebmGeneratorController 30 | 31 | initialFiles = sys.argv[1:] 32 | webmGenerator = WebmGeneratorController(initialFiles) 33 | webmGenerator() 34 | 35 | del webmGenerator 36 | except Exception as e: 37 | logging.error('Startup Exception',exc_info=e) 38 | logging.error(traceback.format_exc()) 39 | 40 | logging.info('DONE') 41 | sys.exit() 42 | os.kill() 43 | --------------------------------------------------------------------------------