├── 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 |
25 | Right
26 | Left
27 | |
28 | Seek forwards and backwards |
29 |
30 |
31 |
32 |
33 | Shift + Left
34 | Shift + Right
35 | |
36 | Large seek forwards and backwards |
37 |
38 |
39 |
40 |
41 | ,
42 | .
43 | |
44 | Single frame step |
45 |
46 |
47 |
48 |
49 | Ctrl + Left
50 | Ctrl + Right
51 | |
52 | Jump between subclips |
53 |
54 |
55 |
56 |
57 | Space
58 | |
59 | Play/Pause |
60 |
61 |
62 |
63 |
64 | F
65 | |
66 | Jump to random point |
67 |
68 |
69 |
70 |
71 | Y
72 | |
73 | Accept subclip preview, Jump to random point and add new subclip preview |
74 |
75 |
76 |
77 |
78 | U
79 | |
80 | Jump to random point and add new subclip preview |
81 |
82 |
83 |
84 |
85 |
86 |
87 | V
88 | |
89 | Start/End a new selection at the current time |
90 |
91 |
92 |
93 |
94 | B
95 | |
96 | Add a new subclip at the current time |
97 |
98 |
99 |
100 |
101 | C
102 | |
103 | Cut/Slice current sublip into two at current time |
104 |
105 |
106 |
107 |
108 | D
109 | |
110 | Delete Subclip at current time |
111 |
112 |
113 |
114 |
115 | M
116 | |
117 | Merge all subclips under current selection |
118 |
119 |
120 |
121 |
122 | Ctrl + F
123 | |
124 | Jump to next file matching text search |
125 |
126 |
127 |
128 |
129 | Ctrl + R
130 | |
131 | Jump to random file matching text search |
132 |
133 |
134 |
135 |
136 | Ctrl + A
137 | |
138 | Select whole video as new subclip |
139 |
140 |
141 |
142 |
143 | Q
144 | E
145 | |
146 | Jump to next/previous file |
147 |
148 |
149 |
150 |
151 | R
152 | |
153 | Jump to random file |
154 |
155 |
156 |
157 |
158 |
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 |
--------------------------------------------------------------------------------