├── LICENSE
├── README.md
├── blurryCardBackground.js
├── pwPlayer - Browser.js
├── pwPlayer - Fullscreen v0.5.js
├── pwPlayer - Oculus v0.5.js
├── pwPlayer - PiP v0.5.js
└── pwPlayer.js
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Philip Wang
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Stash Custom Javascripts - Unexpectably Powerful !
2 |
3 |
4 | #### This repo contains custom javascripts to be used in the amazing [StashApp](https://github.com/stashapp/stash), which empower you to manage all your special video collections. Credits to all the incredible, talented and hardworking programmers who make the StashApp so elegant and useful !
5 |
6 | File to use:
7 | * pwPlayer.js : This is the latest development.
8 | * pwPlayer - PiP v0.5.js : This This is pre-configured js file for playing videos in Picture-in-Picture mode. Default size is 800x450, you can change it in the script.
9 | * pwPlayer - Fullscreen v0.5.js : This This is pre-configured js file for playing videos in Fullscreen mode.
10 | * pwPlayer - Browser v0.5.js : This This is pre-configured js file for playing videos in Browser mode, which the video will fill up the available browser space, but it will not make the browser fullscreen.
11 | * pwPlayer - Oculus v0.5.js : This is pre-configured js file for Oculus Browser. Every video will be played in full screen mode. Works great for VR videos! In fullscreen you need to choose the VR mode like 180 and 3D side by side. The browser will not remember it. When the playing is done, click on the pause button and you will be back to the scene wall immediately.
12 | * blurryCardBackground.js: Fill scene/movie/image/gallery/studio cards with blurry background. Inspired by CJ in Discord channel.
13 |
14 | ### Reason to create a repo just for this:
15 |
16 | StashApp is an excellent video management system for all your desires. It has many great features, but playing videos is a little short.
17 | I made an AutoIt program just to enhance its video playback capability, yet that's not cross-platform and the program has problems here and there.
18 | Well, in Stash version v0.18 there is a new feature called "Custom Javascript". I saw someone submitted a script to enable IINA player on MacOS. I was curious and started to modify that script. Little did I knew that "Custom Javascript" in StashApp can be so powerful ! I can actually utilize the full pontential of the browsers, Stash and platforms !
19 |
20 | -----
21 |
22 | ### To install my scripts
23 | Just copy the content of the script you choose and paste them into the "Settings->Interface->Custom Javascript". Reload the browser and done.
24 | Please don't paste 2 scripts into it, or mixing different scripts together. It usually won't work. Or you are also familiar with Javascript, then you can do your own mixing.
25 |
26 | -----
27 |
28 | ### pwPlayer.js - Scene Card Quick Player
29 |
30 | This Javascript will create a "Play" button in each scene card. You can click on it and the video for that scene will be played right away. Click on the video again, then you are back to the scene list.
31 | #### Features
32 | * You Use different mode to watch the video
33 | - browser mode: video will be played inside the browser.
34 | - browserfull mode: video will be played inside the browser, but in fullscreen mode.
35 | - player mode: script will try to send the video to an external player like VLC. It works well in Android, but others need more work.
36 | * One click to end the play back, and you will see the scene selection again. Very convenient.
37 | * Worked and tested in all 3 major browsers: Chrome, Firefox and Ms Edge.
38 | * Now it can show detailed information for the scene file:
39 | *
40 | #### Versions
41 | - v0.5 Added "browserpip" mode, which the video will be played in a draggable window.
42 | - v0.4 The browser and browserfull mode is better. Oculus browser now can play videos in browserfull mode. Useful for VR videos.
43 | - v0.3 Add "browserfull" mode and test fullscreen feature in all 3 browsers.
44 | - v0.2 Seperate the code with different mode: "browsers" and "player".
45 | - v0.1 Add file details to the on mouse hover event.
46 |
47 | ### blurryCardBackground - Add blurry background to scene/movie/gallery/image/studio.
48 | It fills up the background with blurry images like this:
49 |
50 | CJ made the original CSS and javascript. I completed the script and make it works in most pages.
51 | This script is better to be used alone, don't mix it up.
52 |
53 |
54 |
--------------------------------------------------------------------------------
/blurryCardBackground.js:
--------------------------------------------------------------------------------
1 | /* Created by CJ|500+ TB Stash in Discord channel.
2 | This script will make each scene/movie/gallery/performer/studio card's background to be blurry.
3 | To use it, just copy and paste the code into Stash->Settings->Interface->Custome Javascript.
4 | Then refresh the browser.
5 |
6 | This script is intended to be use as the only script in the custom javascript setting.
7 | */
8 |
9 | // settings
10 | const debug = true;
11 |
12 | function log(str){
13 | if(debug)console.log(str);
14 | }
15 | log("program starts.");
16 | // style
17 | const blurry_style = document.createElement("style");
18 | blurry_style.innerHTML = `
19 | .thumbnail-section::after {
20 | -webkit-backdrop-filter: blur(5px);
21 | backdrop-filter: blur(5px);
22 |
23 | content: "";
24 | display: block;
25 | position: absolute;
26 | width: 100%; height: 100%;
27 | top: 0;
28 | }
29 |
30 | .thumbnail-section {
31 | position: relative;
32 | background-position: center;
33 | }
34 | `;
35 |
36 | // wait for last elm of page
37 | const blurry_config = { subtree: true, childList: true };
38 | const blurry_WaitElm = "span[class^='filter-container']";
39 |
40 | const blurry_waitForElm = (selector) => {
41 | return new Promise(resolve => {
42 | if (document.querySelector(selector)) {
43 | return resolve(document.querySelector(selector));
44 | }
45 |
46 | const observer = new MutationObserver(mutations => {
47 | if (document.querySelector(selector)) {
48 | resolve(document.querySelector(selector));
49 | observer.disconnect();
50 | }
51 | });
52 | observer.observe(document.body, blurry_config);
53 | });
54 | };
55 |
56 | // initial
57 | var blurry_node = null;
58 | if(addStyle()){
59 | blurry_waitForElm(blurry_WaitElm).then(() => {
60 | addImageSource();
61 | });
62 | }
63 | // route
64 | let previousUrl = window.location.href;
65 | const observer = new MutationObserver(function (mutations) {
66 | if (window.location.href !== previousUrl) {
67 | previousUrl = window.location.href;
68 | if (blurry_node != null){
69 | document.head.removeChild(blurry_node);
70 | blurry_node = null;
71 | }
72 |
73 | if(addStyle()){
74 | blurry_waitForElm(blurry_WaitElm).then(() => {
75 | addImageSource();
76 | });
77 | }
78 | }
79 | });
80 |
81 | observer.observe(document, blurry_config);
82 |
83 | function addStyle(){
84 | category = getCat()
85 | switch (category){
86 | case "scene":
87 | case "movie":
88 | case "image":
89 | case "gallery":
90 | case "performers":
91 | blurry_node = document.head.appendChild(blurry_style);
92 | return true;
93 | default:
94 | return false;
95 | }
96 | }
97 |
98 |
99 | // Main function.
100 | function addImageSource() {
101 | cat = getCat();
102 |
103 | log( "cat:" + cat );
104 | if (cat == "others"){
105 | // just in case
106 | document.head.removeChild(blurry_node);
107 | return;
108 | }
109 |
110 | // Add style here
111 | log("adding images.")
112 | // Galleries?
113 | var cards = document.querySelectorAll("div[class^='" + cat +"-card ']");
114 | if (cards.length == 0) {
115 | if ( cat == "gallery" ){
116 | cat = "image";
117 | cards = document.querySelectorAll("div[class^='image-card ']");
118 | }else if(cat == "movie"){
119 | cat = "scene";
120 | cards = document.querySelectorAll("div[class^='scene-card ']");
121 | }
122 | }
123 | log("cards:" + cards.length)
124 | cards.forEach(card => {
125 | thumbnail_section = card.querySelector('.thumbnail-section');
126 | if (thumbnail_section === null ){
127 | log("no thumb section.");
128 | return;
129 | }
130 | switch (cat){
131 | case "gallery":
132 | case "movie":
133 | case "performer":
134 | preview_image = card.querySelector("img." + cat + "-card-image");
135 | break;
136 | default:
137 | preview_image = card.querySelector("img." + cat + "-card-preview-image");
138 | }
139 | if (preview_image === null){
140 | log( "preview image is null. " );
141 | return;
142 | }
143 | // patch code 1
144 | if (cat == "scene"){
145 | preview_video = card.querySelector("video.scene-card-preview-video");
146 | if (preview_video !== null){
147 | preview_video.style.zIndex = "2";
148 | }
149 | }
150 |
151 | preview_image.style.position = "relative";
152 | preview_image.style.zIndex = "1";
153 |
154 | thumbnail_section.style.backgroundImage = "url(" + preview_image.src + ")";
155 |
156 | });
157 | };
158 |
159 | function getCat(){
160 | url = new URL(window.location.href);
161 | path = String(url.pathname);
162 | log("path:" + path);
163 | const catArray = path.match( /\/[a-z]+/ )
164 | if (catArray == null ) return "others";
165 | if(catArray[0] == "/galleries") {
166 | return "gallery";
167 | }else{
168 | // get rid of the '/' in the beginning and "s" in the end
169 | cat = catArray[0].slice(1,-1);
170 | if (["scene", "movie", "image", "performer" ].indexOf(cat) !== -1)
171 | return cat;
172 | return "others";
173 | }
174 | }
175 |
--------------------------------------------------------------------------------
/pwPlayer - Browser.js:
--------------------------------------------------------------------------------
1 | /* Inspired by clangmoyai's IINA player script in github !
2 | This script will add a "Play" button in each scene card.
3 | Allow you to easily play those video files.
4 | To use it, just copy and paste the code into Stash->Settings->Interface->Custome Javascript.
5 | Then refresh the browser.
6 |
7 | Player mode should be either "browser", "browserfull", "browserpip" or "player"
8 | * browser: The video is played within a tag. It works for most platforms.
9 | * browserfull: The video is played in the browser, but in full screen.
10 | * browserpip: The video is played in a small window in the front.
11 | * player: The browser will try to send the steam link to an external player.
12 | Player mode is still buggy, but it should work in android.
13 | A special use for browserfull mode, is to use Oculus Browser to see the content of Stash,
14 | then use "Play" to open the scene video in fullscreen quickly. It's a great way to view Stash and play scene files in Oculus Quest.
15 | Version 0.5
16 | */
17 |
18 | // settings
19 | const debug = false;
20 |
21 | const pwPlayer_mode = "browser";
22 |
23 | function log(str){
24 | if(debug)console.log(str);
25 | }
26 | log("program starts.");
27 | log("build001");
28 | // track mouse y position
29 | var pwPlayer_mouseY = 0;
30 | document.body.onmousemove = (e) => {
31 | pwPlayer_mouseY = e.offsetY;
32 | }
33 |
34 |
35 | const pwPlayer_settings = {
36 | // Path fixes for different OS. For local only.
37 | "Windows":{
38 | // Use vlc to handle local files.
39 | "urlScheme": "vlc://",
40 | // double backsplashes need 4 backslashes.
41 | "replacePath": ["\\\\", "/"],
42 | },
43 | "Android":{
44 | // Not used.
45 | "urlScheme": "file:///",
46 | "replacePath": ["", ""],
47 | },
48 | "iOS":{
49 | // Not use
50 | "urlScheme": "file://",
51 | "replacePath": ["", ""],
52 | },
53 | "Linux":{
54 | // not use
55 | "urlScheme": "file://",
56 | "replacePath": ["", ""],
57 | },
58 | "MacOS":{
59 | // For local iina player.
60 | "urlScheme": "iina://weblink?url=file://",
61 | // Or VLC: "urlScheme": "vlc-x-callback://x-callback-url/stream?url=file://"
62 | "replacePath": ["", ""],
63 | },
64 | "Oculus":{
65 | // not use.
66 | "urlScheme": "file://",
67 | "replacePath": ["", ""],
68 | },
69 | "Others":{
70 | // not use.
71 | "urlScheme": "file://",
72 | "replacePath": ["", ""],
73 | }
74 | };
75 |
76 | // style
77 | const pwPlayer_style = document.createElement("style");
78 | pwPlayer_style.innerHTML = `
79 | .pwPlayer_button {
80 | border-radius: 3.5px;
81 | cursor: pointer;
82 | padding: 2px 9px 3px 13px;
83 | }
84 | .pwPlayer_button:hover {
85 | background-color: rgba(138, 155, 168, .15);
86 | }
87 | .pwPlayer_button svg {
88 | fill: currentColor;
89 | width: 1em;
90 | vertical-align: middle;
91 | }
92 | .pwPlayer_button span {
93 | font-size: 13px;
94 | font-weight: 500;
95 | letter-spacing: 0.1em;
96 | color: currentColor;
97 | vertical-align: middle;
98 | margin-left: 3px;
99 | }
100 | #pwPlayer_videoDiv{
101 | background: black;
102 | position: absolute;
103 | top: 0px;
104 | left: 0px;
105 | width: 100%;
106 | height: 100%;
107 | z-index: 1040;
108 | }
109 | #pwPlayer_video{
110 | object-fit: contain;
111 | object-position: center;
112 | cursor: pointer;
113 | position: relative;
114 | width: 100%;
115 | height: 100%;
116 | }
117 | #pwPlayer_videoDivPIP{
118 | background: black;
119 | position: absolute;
120 | top: 0px;
121 | left: 0px;
122 | width: 800px;
123 | height: 460px;
124 | z-index: 1040;
125 | }
126 | #pwPlayer_videoDivPIPheader{
127 | padding: 10px;
128 | cursor: move;
129 | z-index: 1040;
130 | background-color: #202124;
131 | color: #ffffff;
132 | }
133 | }
134 | `;
135 |
136 | // Only need to call once.
137 | const pwPlayer_OS = pwPlayer_getOS();
138 | log("OS: " + pwPlayer_OS);
139 |
140 | document.head.appendChild(pwPlayer_style);
141 |
142 | // api
143 | const pwPlayer_getSceneDetails = async href => {
144 | const regex = /\/scenes\/(\d+)\?/,
145 | sceneId = regex.exec(href)[1],
146 | graphQl = `
147 | {
148 | findScene(id:${sceneId}){
149 | files{
150 | path,
151 | size,
152 | format,
153 | width,
154 | height,
155 | duration,
156 | video_codec,
157 | audio_codec,
158 | frame_rate,
159 | },
160 | date,
161 | }
162 | }
163 | `,
164 | response = await fetch("/graphql", {
165 | method: "POST",
166 | headers: {
167 | "Content-Type": "application/json",
168 | },
169 | body: JSON.stringify({ query: graphQl })
170 | });
171 | return response.json();
172 | };
173 |
174 | const pwPlayer_getSceneInfo = async href => {
175 | const regex = /\/scenes\/(\d+)\?/,
176 | sceneId = regex.exec(href)[1],
177 | graphQl = `{ findScene(id: ${sceneId}) { files { path, basename }, paths{stream} } }`,
178 | response = await fetch("/graphql", {
179 | method: "POST",
180 | headers: {
181 | "Content-Type": "application/json",
182 | },
183 | body: JSON.stringify({ query: graphQl })
184 | });
185 | return response.json();
186 | };
187 |
188 |
189 | function pwPlayer_getOS() {
190 | var uA = window.navigator.userAgent,
191 | os = "Others";
192 | switch(true){
193 | case uA.includes("Win"):
194 | return "Windows";
195 | case uA.includes("Mac"):
196 | return "MacOS";
197 | case uA.includes("Oculus"):
198 | return "Oculus";
199 | case uA.includes("Linux"):
200 | return "Linux";
201 | case uA.includes("Android"):
202 | return "Android";
203 | case uA.includes("X11"):
204 | return "Unix";
205 | default:
206 | return 'Others';
207 | }
208 | }
209 |
210 | function pwPlayer_getBrowser(){
211 | // not using this much.
212 | var userAgent = window.navigator.userAgent;
213 |
214 | switch (true){
215 | case userAgent.includes("OculusBrowser"):
216 | // special detection for Quest 2.
217 | return "oculus";
218 | case userAgent.includes("chrom"):
219 | case userAgent.includes("crios"):
220 | return "chrome";
221 | case userAgent.includes("firefox"):
222 | case userAgent.includes("fxios"):
223 | return "firefox";
224 | case userAgent.includes("safari"):
225 | return "safari";
226 | case userAgent.includes("opr"):
227 | return "opera";
228 | case userAgent.includes("edg"):
229 | return "edge";
230 | default:
231 | return "others";
232 | }
233 | }
234 |
235 | const pwPlayer_config = { subtree: true, childList: true };
236 | const pwPlayer_WaitElm = "div.toast-container.row";
237 | // promise
238 | const pwPlayer_waitForElm = selector => {
239 | return new Promise(resolve => {
240 | if (document.querySelector(selector)) {
241 | return resolve(document.querySelector(selector));
242 | }
243 | const observer = new MutationObserver(mutations => {
244 | if (document.querySelector(selector)) {
245 | resolve(document.querySelector(selector));
246 | observer.disconnect();
247 | }
248 | });
249 | observer.observe(document.body, pwPlayer_config);
250 | });
251 | };
252 |
253 | // initial
254 | pwPlayer_waitForElm(pwPlayer_WaitElm).then(() => {
255 | pwPlayer_addButton();
256 | });
257 |
258 | // route
259 | let previousUrl = "";
260 | const observer = new MutationObserver(function (mutations) {
261 | if (window.location.href !== previousUrl) {
262 | previousUrl = window.location.href;
263 | pwPlayer_waitForElm(pwPlayer_previewElm).then(() => {
264 | pwPlayer_addButton();
265 | });
266 | }
267 | });
268 |
269 | observer.observe(document, pwPlayer_config);
270 |
271 | // main
272 | const pwPlayer_addButton = () => {
273 | const scenes = document.querySelectorAll("div.row > div");
274 | for (const scene of scenes) {
275 | if (scene.querySelector("a.pwPlayer_button") != null) continue;
276 |
277 | const scene_url = scene.querySelector("a.scene-card-link"),
278 | popover = scene.querySelector("div.card-popovers"),
279 | button = document.createElement("a");
280 | button.innerHTML = `
281 |
282 |
284 | Play `;
285 |
286 | button.classList.add("pwPlayer_button");
287 | button.href = "javascript:;";
288 |
289 | button.onclick = () =>{
290 | pwPlayer_getSceneInfo(scene_url.href)
291 | .then((result) =>{
292 | const streamLink = result.data.findScene.paths.stream;
293 | const filePath = result.data.findScene.files[0].path
294 | .replace(pwPlayer_settings[pwPlayer_OS].replacePath[0],
295 | pwPlayer_settings[pwPlayer_OS].replacePath[1]);
296 |
297 |
298 | switch(pwPlayer_mode){
299 | case "browser": // normal browser mode
300 | playVideoInBrowser(streamLink);
301 | break;
302 | case "browserfull": // fullscreen browser mode
303 | playVideoInBrowser(streamLink, true);
304 | break;
305 | case "browserpip": // picture in picture browser mode
306 | playVideoPIP(streamLink);
307 | break;
308 | case "player":
309 | switch (pwPlayer_OS){
310 | case "Mac OS":
311 | // Sample local handling for iina player.
312 | // if you don't have iina player, use "remote" mode instead.
313 | if(debug)alert("playermode. you just click play in MacOS");
314 | if (pwPlayer_mode == "player"){
315 | href = pwPlayer_settings.MacOS.urlScheme +
316 | pwPlayer_settings.MacOS.replacePath[1] +
317 | encodeURIComponent(filePath);
318 | window.open(href);
319 | }
320 | break;
321 | case "Android":
322 | // Special andoid launch with intent
323 | if(debug)alert("playermode. you just click play in Android");
324 | if (button.href == "javascript:;"){
325 | url = new URL(streamLink);
326 | const scheme=url.protocol.slice(0,-1);
327 | url.hash = `Intent;action=android.intent.action.VIEW;scheme=${scheme};type=video/mp4;S.title=${encodeURI(
328 | result.data.findScene.files[0].basename
329 | )};end`;
330 | url.protocol = "intent";
331 | button.href = url.toString();
332 | button.click();
333 | }
334 | break;
335 |
336 | case "iOS":
337 | // Special ios launch
338 | if( button.href == "javascript:;"){
339 | url = new URL();
340 | url.host = "x-callback-url";
341 | url.port = "";
342 | url.pathname = "stream";
343 | url.search = `url=${encodeURIComponent(streamLink)}`;
344 | url.protocol = "vlc-x-callback";
345 | button.href = url.toString();
346 | button.click();
347 | }
348 | break;
349 | case "Oculus":
350 | // use browser built-in player
351 | if (button.href == "javascript:;"){
352 | button.href = streamLink;
353 | button.click();
354 | }
355 | break;
356 | case "Windows":
357 | if(debug)alert("playermode. you just click play in Windows");
358 | if (pwPlayer_mode == "player"){
359 | settings = pwPlayer_settings.Windows;
360 | href = settings.urlScheme + encodeURIComponent(filePath);
361 | window.open(href);
362 | }
363 | break;
364 | default:
365 | } // end of the switch about os
366 | break; // fullscreen browser mode
367 | } // end of switch of mode
368 | });
369 |
370 | }; // end of button onclick envent.
371 |
372 | if (popover) popover.append(button);
373 |
374 | button.onmouseover = () => {
375 | if (button.title.length == 0) {
376 | pwPlayer_getSceneDetails(scene_url.href)
377 | .then((result) => {
378 | // console.log("result: " + JSON.stringify(result));
379 | data = result.data.findScene;
380 | sceneFile = data.files[0];
381 | // log("before title phase.")
382 | title =`Path: ${ WrapStr(sceneFile.path,30)}
383 | Size: ${niceBytes(sceneFile.size)}
384 | Dimensions: ${sceneFile.width}x${sceneFile.height}
385 | Duration: ${toHMS(sceneFile.duration)}
386 | Codecs: ${sceneFile.video_codec}, ${sceneFile.audio_codec}
387 | Frame Rate: ${sceneFile.frame_rate}
388 | ${data.date?"Date: "+data.date : ""}`;
389 | log("title:" + title);
390 | button.title = title;
391 | });
392 | }
393 | }; // end of on mouse move over.
394 |
395 | }; // end of the each scene card loop.
396 | }; // end of pwPlayer_addButton function.
397 |
398 | function WrapStr(s,n){
399 | //
400 | if (s.length <= n) return s;
401 | str = s.substr(0,n)
402 | for (i=n;i('00'+n).slice(-2);
411 | return f(s/3600)+':'+g(f(s/60)%60)+':'+g(s%60);
412 | }
413 |
414 | function niceBytes(x){
415 | let units = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
416 | let l = 0, n = parseInt(x, 10) || 0;
417 | while(n >= 1024 && ++l){ n = n/1024; }
418 | return(n.toFixed(n < 10 && l > 0 ? 1 : 0) + ' ' + units[l]);
419 | }
420 |
421 | function playVideoInBrowser(streamLink, fullscreen = false){
422 | // It adds a video in the front of the body, while PIP adds to the end.
423 |
424 | // close previous video element, if any.
425 | const previousElm = document.body.querySelector(".pwPlayer_videoDiv");
426 | if (previousElm!==null){
427 | document.body.removeChild(previousElm);
428 | }
429 |
430 | var pwPlayer_video_div = document.createElement("div");
431 |
432 | pwPlayer_video_div.id = "pwPlayer_videoDiv";
433 | var pwPlayer_video = document.createElement("video");
434 | pwPlayer_video.id = "pwPlayer_video";
435 | pwPlayer_video.autoplay = true;
436 | pwPlayer_video.controls = true;
437 | pwPlayer_video.src = streamLink;
438 | pwPlayer_video_div.appendChild(pwPlayer_video);
439 | var pwPlayer_divNode = document.body.insertBefore(pwPlayer_video_div, document.body.firstChild);
440 | pwPlayer_video_div.width =window.innerWidth;
441 | pwPlayer_video_div.height =window.innerHeight;
442 |
443 | log("win inner w:" + window.innerWidth);
444 | log("win inner h: "+ window.innerHeight);
445 | log("div width:" + pwPlayer_video_div.width);
446 | log("div height:" + pwPlayer_video_div.height);
447 |
448 | // save the scroll postion.
449 | var pwPlayer_scrollPos;
450 | if (typeof window.pageYOffset != 'undefined') {
451 | pwPlayer_scrollPos = window.pageYOffset;
452 | }
453 | else if (typeof document.compatMode != 'undefined' && document.compatMode != 'BackCompat') {
454 | pwPlayer_scrollPos = document.documentElement.scrollTop;
455 | }
456 | else if (typeof document.body != 'undefined') {
457 | pwPlayer_scrollPos = document.body.scrollTop;
458 | }
459 | log("scroll pos:" + pwPlayer_scrollPos);
460 |
461 | window.scrollTo(0,0);
462 |
463 | // now make it full screen if enabled
464 | if (fullscreen){
465 | doFullScreen();
466 | pwPlayer_video.height = screen.height;
467 | pwPlayer_video.width = screen.width;
468 |
469 | };
470 |
471 | let pwPwPlayer_videoEnd = () =>{
472 | // all video will call this to end.
473 | // pwPlayer_video.pause(); the video usually paused already.
474 | if(inFullScreen()){
475 | exitFullscreen();
476 | }
477 | document.body.removeChild(pwPlayer_divNode);
478 | window.scrollTo(0, pwPlayer_scrollPos);
479 | };
480 |
481 | pwPlayer_video.onerror = () => {
482 | alert("Error playing this video.");
483 | log("Error in playing, scroll pos:" + pwPlayer_scrollPos);
484 | pwPwPlayer_videoEnd();
485 | };
486 |
487 | pwPlayer_video.onended = () => {
488 | // normal ending
489 | log("video reach the end.");
490 | pwPwPlayer_videoEnd();
491 | };
492 |
493 | pwPlayer_video.onpause = (event) => {
494 | if(inFullScreen()){
495 | // in fullscreen mode, mouse in the bottom 1/5, do nothing.
496 | if ( pwPlayer_mouseY+window.screenTop > window.outerHeight*0.8 ){
497 | return;
498 | }else{
499 | log("bingo: mouseY:" + pwPlayer_mouseY + " winTop:" + window.screenTop + " win.OH:" + window.outerHeight);
500 | // exit full screen and prepare to be deleted.
501 | pwPwPlayer_videoEnd();
502 | return;
503 | }
504 | }
505 | // normal video process.
506 | if ( pwPlayer_mouseY > screen.innerHeight*0.8 ) return;
507 |
508 | log("play ends, scroll pos:" + pwPlayer_scrollPos);
509 | pwPwPlayer_videoEnd();
510 | };
511 |
512 | }
513 |
514 | var pwPlayer_DivX=0, pwPlayer_DivY=0;
515 |
516 | function playVideoPIP(streamLink){
517 | // It will show a video in a dragable window
518 | const previousElm = document.body.querySelector(".pwPlayer_videoDivPIP");
519 | if (previousElm!==null){
520 | document.body.removeChild(previousElm);
521 | }
522 | var pwPlayer_video_div = document.createElement("div");
523 | pwPlayer_video_div.id = "pwPlayer_videoDivPIP";
524 | var pwPlayer_PiPHeader = document.createElement("div");
525 | pwPlayer_PiPHeader.id = "pwPlayer_videoDivPIPheader";
526 | pwPlayer_video_div.appendChild(pwPlayer_PiPHeader);
527 | var pwPlayer_video = document.createElement("video");
528 | pwPlayer_video.id = "pwPlayer_video";
529 | pwPlayer_video.autoplay = true;
530 | pwPlayer_video.controls = true;
531 | pwPlayer_video.src = streamLink;
532 | pwPlayer_video_div.appendChild(pwPlayer_video);
533 | var pwPlayer_divNode = document.body.appendChild(pwPlayer_video_div);
534 | x = (pwPlayer_DivX + pwPlayer_video_div.offsetWidth > window.innerWidth) ?
535 | window.innerWidth - pwPlayer_video_div.offsetWidth : pwPlayer_DivX ;
536 | x = (x < 0)? 0 : x;
537 | y = (pwPlayer_DivY + pwPlayer_video_div.offsetHeight> window.innerHeight)?
538 | window.innerHeight - pwPlayer_video_div.offsetHeight : pwPlayer_DivY ;
539 | y = (y < 0)? 0 : y;
540 |
541 |
542 | pwPlayer_video_div.style.top = (window.scrollY + y)+"px";
543 | pwPlayer_video_div.style.left = (window.scrollX + x)+"px";
544 |
545 | // pwPlayer_video_div.width =300;
546 | // pwPlayer_video_div.height =200;
547 |
548 | pwPlayer_video.onpause = () =>{
549 | pipWidth = pwPlayer_video_div.offsetWidth;
550 | pipHeight = pwPlayer_video_div.offsetHeight;
551 |
552 | pwPlayer_DivY = parseInt(pwPlayer_video_div.style.top)-window.scrollY;
553 | pwPlayer_DivX = parseInt(pwPlayer_video_div.style.left)-window.scrollX;
554 |
555 | // log("mouseY:"+_mouseY);
556 | if(inFullScreen()){
557 | // in fullscreen mode, mouse in the bottom 1/5, do nothing.
558 | if ( pwPlayer_mouseY+window.screenTop > window.outerHeight*0.8 ){
559 | return;
560 | }else{
561 | log("bingo: mouseY:" + pwPlayer_mouseY + " winTop:" + window.screenTop + " win.OH:" + window.outerHeight);
562 | // exit full screen and prepare to be deleted.
563 | exitFullscreen();
564 | document.body.removeChild(pwPlayer_divNode);
565 | return;
566 | }
567 | }
568 |
569 | // mouse is inside the pip box, but in the lower control area
570 | if (pwPlayer_mouseY > pipHeight*0.8 ) return;
571 | // Save the previous location
572 | log( "mouseY:"+ pwPlayer_mouseY + " DivY:" + pwPlayer_DivY + " divX:" + pwPlayer_DivX);
573 | // delete it.
574 | document.body.removeChild(pwPlayer_divNode);
575 | };
576 |
577 | pwPlayer_video.onended = () =>{
578 | if(inFullScreen()){
579 | // in fullscreen mode, mouse in the bottom 1/5, do nothing.
580 | if ( pwPlayer_mouseY+window.screenTop > window.outerHeight*0.8 ){
581 | return;
582 | }else{
583 | log("bingo: mouseY:" + pwPlayer_mouseY + " winTop:" + window.screenTop + " win.OH:" + window.outerHeight);
584 | // exit full screen and prepare to be deleted.
585 | exitFullscreen();
586 | document.body.removeChild(pwPlayer_divNode);
587 | return;
588 | }
589 | }
590 | document.body.removeChild(pwPlayer_divNode);
591 | };
592 |
593 | dragPiPElement(pwPlayer_video_div);
594 |
595 | }
596 |
597 |
598 | function dragPiPElement(elmnt) {
599 | var pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
600 | if (document.getElementById(elmnt.id + "header")) {
601 | // if present, the header is where you move the DIV from:
602 | document.getElementById(elmnt.id + "header").onmousedown = dragMouseDown;
603 | } else {
604 | // otherwise, move the DIV from anywhere inside the DIV:
605 | elmnt.onmousedown = dragMouseDown;
606 | }
607 |
608 | function dragMouseDown(e) {
609 | e = e || window.event;
610 | e.preventDefault();
611 | // get the mouse cursor position at startup:
612 | pos3 = e.clientX;
613 | pos4 = e.clientY;
614 | document.onmouseup = closeDragElement;
615 | // call a function whenever the cursor moves:
616 | document.onmousemove = elementDrag;
617 | }
618 |
619 | function elementDrag(e) {
620 | e = e || window.event;
621 | e.preventDefault();
622 | // calculate the new cursor position:
623 | pos1 = pos3 - e.clientX;
624 | pos2 = pos4 - e.clientY;
625 | pos3 = e.clientX;
626 | pos4 = e.clientY;
627 | // set the element's new position:
628 | elmnt.style.top = (elmnt.offsetTop - pos2) + "px";
629 | elmnt.style.left = (elmnt.offsetLeft - pos1) + "px";
630 | }
631 |
632 | function closeDragElement() {
633 | // stop moving when mouse button is released:
634 | document.onmouseup = null;
635 | document.onmousemove = null;
636 | }
637 | }
638 |
639 |
640 | function doFullScreen() {
641 | var element = document.documentElement;
642 | // Check which implementation is available
643 | var requestMethod = element.requestFullScreen ||
644 | element.webkitRequestFullScreen ||
645 | element.mozRequestFullScreen ||
646 | element.msRequestFullscreen;
647 |
648 | if( requestMethod ) {
649 | // log("fullscreen method found: " + requestMethod);
650 | requestMethod.apply( element );
651 | }else{
652 | // log("fullscreen method not found");
653 | }
654 | ;
655 |
656 | }
657 |
658 | function exitFullscreen(){
659 | var element = document;
660 | // Check which implementation is available
661 | var requestMethod = element.exitFullScreen ||
662 | element.webkitExitFullscreen ||
663 | element.mozCancelFullScreen ||
664 | element.msExitFullscreen;
665 |
666 | if( requestMethod ) {
667 | log ("have method to exit full screen." + requestMethod.toString());
668 | requestMethod.apply( element );
669 | }else{
670 | log ("no method to exit full screen.");
671 | }
672 | }
673 |
674 | function inFullScreen(){
675 | var doc = window.document;
676 | return !(!doc.fullscreenElement
677 | && !doc.mozFullScreenElement
678 | && !doc.webkitFullscreenElement
679 | && !doc.msFullscreenElement);
680 | }
--------------------------------------------------------------------------------
/pwPlayer - Fullscreen v0.5.js:
--------------------------------------------------------------------------------
1 | /* Inspired by clangmoyai's IINA player script in github !
2 | This script will add a "Play" button in each scene card.
3 | Allow you to easily play those video files.
4 | To use it, just copy and paste the code into Stash->Settings->Interface->Custome Javascript.
5 | Then refresh the browser.
6 |
7 | Player mode should be either "browser", "browserfull", "browserpip" or "player"
8 | * browser: The video is played within a tag. It works for most platforms.
9 | * browserfull: The video is played in the browser, but in full screen.
10 | * browserpip: The video is played in a small window in the front.
11 | * player: The browser will try to send the steam link to an external player.
12 | Player mode is still buggy, but it should work in android.
13 | A special use for browserfull mode, is to use Oculus Browser to see the content of Stash,
14 | then use "Play" to open the scene video in fullscreen quickly. It's a great way to view Stash and play scene files in Oculus Quest.
15 | Version 0.5
16 | */
17 |
18 | // settings
19 | const debug = false;
20 |
21 | const pwPlayer_mode = "browserfull";
22 |
23 | function log(str){
24 | if(debug)console.log(str);
25 | }
26 | log("program starts.");
27 | log("build001");
28 | // track mouse y position
29 | var pwPlayer_mouseY = 0;
30 | document.body.onmousemove = (e) => {
31 | pwPlayer_mouseY = e.offsetY;
32 | }
33 |
34 |
35 | const pwPlayer_settings = {
36 | // Path fixes for different OS. For local only.
37 | "Windows":{
38 | // Use vlc to handle local files.
39 | "urlScheme": "vlc://",
40 | // double backsplashes need 4 backslashes.
41 | "replacePath": ["\\\\", "/"],
42 | },
43 | "Android":{
44 | // Not used.
45 | "urlScheme": "file:///",
46 | "replacePath": ["", ""],
47 | },
48 | "iOS":{
49 | // Not use
50 | "urlScheme": "file://",
51 | "replacePath": ["", ""],
52 | },
53 | "Linux":{
54 | // not use
55 | "urlScheme": "file://",
56 | "replacePath": ["", ""],
57 | },
58 | "MacOS":{
59 | // For local iina player.
60 | "urlScheme": "iina://weblink?url=file://",
61 | // Or VLC: "urlScheme": "vlc-x-callback://x-callback-url/stream?url=file://"
62 | "replacePath": ["", ""],
63 | },
64 | "Oculus":{
65 | // not use.
66 | "urlScheme": "file://",
67 | "replacePath": ["", ""],
68 | },
69 | "Others":{
70 | // not use.
71 | "urlScheme": "file://",
72 | "replacePath": ["", ""],
73 | }
74 | };
75 |
76 | // style
77 | const pwPlayer_style = document.createElement("style");
78 | pwPlayer_style.innerHTML = `
79 | .pwPlayer_button {
80 | border-radius: 3.5px;
81 | cursor: pointer;
82 | padding: 2px 9px 3px 13px;
83 | }
84 | .pwPlayer_button:hover {
85 | background-color: rgba(138, 155, 168, .15);
86 | }
87 | .pwPlayer_button svg {
88 | fill: currentColor;
89 | width: 1em;
90 | vertical-align: middle;
91 | }
92 | .pwPlayer_button span {
93 | font-size: 13px;
94 | font-weight: 500;
95 | letter-spacing: 0.1em;
96 | color: currentColor;
97 | vertical-align: middle;
98 | margin-left: 3px;
99 | }
100 | #pwPlayer_videoDiv{
101 | background: black;
102 | position: absolute;
103 | top: 0px;
104 | left: 0px;
105 | width: 100%;
106 | height: 100%;
107 | z-index: 1040;
108 | }
109 | #pwPlayer_video{
110 | object-fit: contain;
111 | object-position: center;
112 | cursor: pointer;
113 | position: relative;
114 | width: 100%;
115 | height: 100%;
116 | }
117 | #pwPlayer_videoDivPIP{
118 | background: black;
119 | position: absolute;
120 | top: 0px;
121 | left: 0px;
122 | width: 800px;
123 | height: 460px;
124 | z-index: 1040;
125 | }
126 | #pwPlayer_videoDivPIPheader{
127 | padding: 10px;
128 | cursor: move;
129 | z-index: 1040;
130 | background-color: #202124;
131 | color: #ffffff;
132 | }
133 | }
134 | `;
135 |
136 | // Only need to call once.
137 | const pwPlayer_OS = pwPlayer_getOS();
138 | log("OS: " + pwPlayer_OS);
139 |
140 | document.head.appendChild(pwPlayer_style);
141 |
142 | // api
143 | const pwPlayer_getSceneDetails = async href => {
144 | const regex = /\/scenes\/(\d+)\?/,
145 | sceneId = regex.exec(href)[1],
146 | graphQl = `
147 | {
148 | findScene(id:${sceneId}){
149 | files{
150 | path,
151 | size,
152 | format,
153 | width,
154 | height,
155 | duration,
156 | video_codec,
157 | audio_codec,
158 | frame_rate,
159 | },
160 | date,
161 | }
162 | }
163 | `,
164 | response = await fetch("/graphql", {
165 | method: "POST",
166 | headers: {
167 | "Content-Type": "application/json",
168 | },
169 | body: JSON.stringify({ query: graphQl })
170 | });
171 | return response.json();
172 | };
173 |
174 | const pwPlayer_getSceneInfo = async href => {
175 | const regex = /\/scenes\/(\d+)\?/,
176 | sceneId = regex.exec(href)[1],
177 | graphQl = `{ findScene(id: ${sceneId}) { files { path, basename }, paths{stream} } }`,
178 | response = await fetch("/graphql", {
179 | method: "POST",
180 | headers: {
181 | "Content-Type": "application/json",
182 | },
183 | body: JSON.stringify({ query: graphQl })
184 | });
185 | return response.json();
186 | };
187 |
188 |
189 | function pwPlayer_getOS() {
190 | var uA = window.navigator.userAgent,
191 | os = "Others";
192 | switch(true){
193 | case uA.includes("Win"):
194 | return "Windows";
195 | case uA.includes("Mac"):
196 | return "MacOS";
197 | case uA.includes("Oculus"):
198 | return "Oculus";
199 | case uA.includes("Linux"):
200 | return "Linux";
201 | case uA.includes("Android"):
202 | return "Android";
203 | case uA.includes("X11"):
204 | return "Unix";
205 | default:
206 | return 'Others';
207 | }
208 | }
209 |
210 | function pwPlayer_getBrowser(){
211 | // not using this much.
212 | var userAgent = window.navigator.userAgent;
213 |
214 | switch (true){
215 | case userAgent.includes("OculusBrowser"):
216 | // special detection for Quest 2.
217 | return "oculus";
218 | case userAgent.includes("chrom"):
219 | case userAgent.includes("crios"):
220 | return "chrome";
221 | case userAgent.includes("firefox"):
222 | case userAgent.includes("fxios"):
223 | return "firefox";
224 | case userAgent.includes("safari"):
225 | return "safari";
226 | case userAgent.includes("opr"):
227 | return "opera";
228 | case userAgent.includes("edg"):
229 | return "edge";
230 | default:
231 | return "others";
232 | }
233 | }
234 |
235 | const pwPlayer_config = { subtree: true, childList: true };
236 | const pwPlayer_WaitElm = "div.toast-container.row";
237 | // promise
238 | const pwPlayer_waitForElm = selector => {
239 | return new Promise(resolve => {
240 | if (document.querySelector(selector)) {
241 | return resolve(document.querySelector(selector));
242 | }
243 | const observer = new MutationObserver(mutations => {
244 | if (document.querySelector(selector)) {
245 | resolve(document.querySelector(selector));
246 | observer.disconnect();
247 | }
248 | });
249 | observer.observe(document.body, pwPlayer_config);
250 | });
251 | };
252 |
253 | // initial
254 | pwPlayer_waitForElm(pwPlayer_WaitElm).then(() => {
255 | pwPlayer_addButton();
256 | });
257 |
258 | // route
259 | let previousUrl = "";
260 | const observer = new MutationObserver(function (mutations) {
261 | if (window.location.href !== previousUrl) {
262 | previousUrl = window.location.href;
263 | pwPlayer_waitForElm(pwPlayer_previewElm).then(() => {
264 | pwPlayer_addButton();
265 | });
266 | }
267 | });
268 |
269 | observer.observe(document, pwPlayer_config);
270 |
271 | // main
272 | const pwPlayer_addButton = () => {
273 | const scenes = document.querySelectorAll("div.row > div");
274 | for (const scene of scenes) {
275 | if (scene.querySelector("a.pwPlayer_button") != null) continue;
276 |
277 | const scene_url = scene.querySelector("a.scene-card-link"),
278 | popover = scene.querySelector("div.card-popovers"),
279 | button = document.createElement("a");
280 | button.innerHTML = `
281 |
282 |
284 | Play `;
285 |
286 | button.classList.add("pwPlayer_button");
287 | button.href = "javascript:;";
288 |
289 | button.onclick = () =>{
290 | pwPlayer_getSceneInfo(scene_url.href)
291 | .then((result) =>{
292 | const streamLink = result.data.findScene.paths.stream;
293 | const filePath = result.data.findScene.files[0].path
294 | .replace(pwPlayer_settings[pwPlayer_OS].replacePath[0],
295 | pwPlayer_settings[pwPlayer_OS].replacePath[1]);
296 |
297 |
298 | switch(pwPlayer_mode){
299 | case "browser": // normal browser mode
300 | playVideoInBrowser(streamLink);
301 | break;
302 | case "browserfull": // fullscreen browser mode
303 | playVideoInBrowser(streamLink, true);
304 | break;
305 | case "browserpip": // picture in picture browser mode
306 | playVideoPIP(streamLink);
307 | break;
308 | case "player":
309 | switch (pwPlayer_OS){
310 | case "Mac OS":
311 | // Sample local handling for iina player.
312 | // if you don't have iina player, use "remote" mode instead.
313 | if(debug)alert("playermode. you just click play in MacOS");
314 | if (pwPlayer_mode == "player"){
315 | href = pwPlayer_settings.MacOS.urlScheme +
316 | pwPlayer_settings.MacOS.replacePath[1] +
317 | encodeURIComponent(filePath);
318 | window.open(href);
319 | }
320 | break;
321 | case "Android":
322 | // Special andoid launch with intent
323 | if(debug)alert("playermode. you just click play in Android");
324 | if (button.href == "javascript:;"){
325 | url = new URL(streamLink);
326 | const scheme=url.protocol.slice(0,-1);
327 | url.hash = `Intent;action=android.intent.action.VIEW;scheme=${scheme};type=video/mp4;S.title=${encodeURI(
328 | result.data.findScene.files[0].basename
329 | )};end`;
330 | url.protocol = "intent";
331 | button.href = url.toString();
332 | button.click();
333 | }
334 | break;
335 |
336 | case "iOS":
337 | // Special ios launch
338 | if( button.href == "javascript:;"){
339 | url = new URL();
340 | url.host = "x-callback-url";
341 | url.port = "";
342 | url.pathname = "stream";
343 | url.search = `url=${encodeURIComponent(streamLink)}`;
344 | url.protocol = "vlc-x-callback";
345 | button.href = url.toString();
346 | button.click();
347 | }
348 | break;
349 | case "Oculus":
350 | // use browser built-in player
351 | if (button.href == "javascript:;"){
352 | button.href = streamLink;
353 | button.click();
354 | }
355 | break;
356 | case "Windows":
357 | if(debug)alert("playermode. you just click play in Windows");
358 | if (pwPlayer_mode == "player"){
359 | settings = pwPlayer_settings.Windows;
360 | href = settings.urlScheme + encodeURIComponent(filePath);
361 | window.open(href);
362 | }
363 | break;
364 | default:
365 | } // end of the switch about os
366 | break; // fullscreen browser mode
367 | } // end of switch of mode
368 | });
369 |
370 | }; // end of button onclick envent.
371 |
372 | if (popover) popover.append(button);
373 |
374 | button.onmouseover = () => {
375 | if (button.title.length == 0) {
376 | pwPlayer_getSceneDetails(scene_url.href)
377 | .then((result) => {
378 | // console.log("result: " + JSON.stringify(result));
379 | data = result.data.findScene;
380 | sceneFile = data.files[0];
381 | // log("before title phase.")
382 | title =`Path: ${ WrapStr(sceneFile.path,30)}
383 | Size: ${niceBytes(sceneFile.size)}
384 | Dimensions: ${sceneFile.width}x${sceneFile.height}
385 | Duration: ${toHMS(sceneFile.duration)}
386 | Codecs: ${sceneFile.video_codec}, ${sceneFile.audio_codec}
387 | Frame Rate: ${sceneFile.frame_rate}
388 | ${data.date?"Date: "+data.date : ""}`;
389 | log("title:" + title);
390 | button.title = title;
391 | });
392 | }
393 | }; // end of on mouse move over.
394 |
395 | }; // end of the each scene card loop.
396 | }; // end of pwPlayer_addButton function.
397 |
398 | function WrapStr(s,n){
399 | //
400 | if (s.length <= n) return s;
401 | str = s.substr(0,n)
402 | for (i=n;i('00'+n).slice(-2);
411 | return f(s/3600)+':'+g(f(s/60)%60)+':'+g(s%60);
412 | }
413 |
414 | function niceBytes(x){
415 | let units = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
416 | let l = 0, n = parseInt(x, 10) || 0;
417 | while(n >= 1024 && ++l){ n = n/1024; }
418 | return(n.toFixed(n < 10 && l > 0 ? 1 : 0) + ' ' + units[l]);
419 | }
420 |
421 | function playVideoInBrowser(streamLink, fullscreen = false){
422 | // It adds a video in the front of the body, while PIP adds to the end.
423 |
424 | // close previous video element, if any.
425 | const previousElm = document.body.querySelector(".pwPlayer_videoDiv");
426 | if (previousElm!==null){
427 | document.body.removeChild(previousElm);
428 | }
429 |
430 | var pwPlayer_video_div = document.createElement("div");
431 |
432 | pwPlayer_video_div.id = "pwPlayer_videoDiv";
433 | var pwPlayer_video = document.createElement("video");
434 | pwPlayer_video.id = "pwPlayer_video";
435 | pwPlayer_video.autoplay = true;
436 | pwPlayer_video.controls = true;
437 | pwPlayer_video.src = streamLink;
438 | pwPlayer_video_div.appendChild(pwPlayer_video);
439 | var pwPlayer_divNode = document.body.insertBefore(pwPlayer_video_div, document.body.firstChild);
440 | pwPlayer_video_div.width =window.innerWidth;
441 | pwPlayer_video_div.height =window.innerHeight;
442 |
443 | log("win inner w:" + window.innerWidth);
444 | log("win inner h: "+ window.innerHeight);
445 | log("div width:" + pwPlayer_video_div.width);
446 | log("div height:" + pwPlayer_video_div.height);
447 |
448 | // save the scroll postion.
449 | var pwPlayer_scrollPos;
450 | if (typeof window.pageYOffset != 'undefined') {
451 | pwPlayer_scrollPos = window.pageYOffset;
452 | }
453 | else if (typeof document.compatMode != 'undefined' && document.compatMode != 'BackCompat') {
454 | pwPlayer_scrollPos = document.documentElement.scrollTop;
455 | }
456 | else if (typeof document.body != 'undefined') {
457 | pwPlayer_scrollPos = document.body.scrollTop;
458 | }
459 | log("scroll pos:" + pwPlayer_scrollPos);
460 |
461 | window.scrollTo(0,0);
462 |
463 | // now make it full screen if enabled
464 | if (fullscreen){
465 | doFullScreen();
466 | pwPlayer_video.height = screen.height;
467 | pwPlayer_video.width = screen.width;
468 |
469 | };
470 |
471 | let pwPwPlayer_videoEnd = () =>{
472 | // all video will call this to end.
473 | // pwPlayer_video.pause(); the video usually paused already.
474 | if(inFullScreen()){
475 | exitFullscreen();
476 | }
477 | document.body.removeChild(pwPlayer_divNode);
478 | window.scrollTo(0, pwPlayer_scrollPos);
479 | };
480 |
481 | pwPlayer_video.onerror = () => {
482 | alert("Error playing this video.");
483 | log("Error in playing, scroll pos:" + pwPlayer_scrollPos);
484 | pwPwPlayer_videoEnd();
485 | };
486 |
487 | pwPlayer_video.onended = () => {
488 | // normal ending
489 | log("video reach the end.");
490 | pwPwPlayer_videoEnd();
491 | };
492 |
493 | pwPlayer_video.onpause = (event) => {
494 | if(inFullScreen()){
495 | // in fullscreen mode, mouse in the bottom 1/5, do nothing.
496 | if ( pwPlayer_mouseY+window.screenTop > window.outerHeight*0.8 ){
497 | return;
498 | }else{
499 | log("bingo: mouseY:" + pwPlayer_mouseY + " winTop:" + window.screenTop + " win.OH:" + window.outerHeight);
500 | // exit full screen and prepare to be deleted.
501 | pwPwPlayer_videoEnd();
502 | return;
503 | }
504 | }
505 | // normal video process.
506 | if ( pwPlayer_mouseY > screen.innerHeight*0.8 ) return;
507 |
508 | log("play ends, scroll pos:" + pwPlayer_scrollPos);
509 | pwPwPlayer_videoEnd();
510 | };
511 |
512 | }
513 |
514 | var pwPlayer_DivX=0, pwPlayer_DivY=0;
515 |
516 | function playVideoPIP(streamLink){
517 | // It will show a video in a dragable window
518 | const previousElm = document.body.querySelector(".pwPlayer_videoDivPIP");
519 | if (previousElm!==null){
520 | document.body.removeChild(previousElm);
521 | }
522 | var pwPlayer_video_div = document.createElement("div");
523 | pwPlayer_video_div.id = "pwPlayer_videoDivPIP";
524 | var pwPlayer_PiPHeader = document.createElement("div");
525 | pwPlayer_PiPHeader.id = "pwPlayer_videoDivPIPheader";
526 | pwPlayer_video_div.appendChild(pwPlayer_PiPHeader);
527 | var pwPlayer_video = document.createElement("video");
528 | pwPlayer_video.id = "pwPlayer_video";
529 | pwPlayer_video.autoplay = true;
530 | pwPlayer_video.controls = true;
531 | pwPlayer_video.src = streamLink;
532 | pwPlayer_video_div.appendChild(pwPlayer_video);
533 | var pwPlayer_divNode = document.body.appendChild(pwPlayer_video_div);
534 | x = (pwPlayer_DivX + pwPlayer_video_div.offsetWidth > window.innerWidth) ?
535 | window.innerWidth - pwPlayer_video_div.offsetWidth : pwPlayer_DivX ;
536 | x = (x < 0)? 0 : x;
537 | y = (pwPlayer_DivY + pwPlayer_video_div.offsetHeight> window.innerHeight)?
538 | window.innerHeight - pwPlayer_video_div.offsetHeight : pwPlayer_DivY ;
539 | y = (y < 0)? 0 : y;
540 |
541 |
542 | pwPlayer_video_div.style.top = (window.scrollY + y)+"px";
543 | pwPlayer_video_div.style.left = (window.scrollX + x)+"px";
544 |
545 | // pwPlayer_video_div.width =300;
546 | // pwPlayer_video_div.height =200;
547 |
548 | pwPlayer_video.onpause = () =>{
549 | pipWidth = pwPlayer_video_div.offsetWidth;
550 | pipHeight = pwPlayer_video_div.offsetHeight;
551 |
552 | pwPlayer_DivY = parseInt(pwPlayer_video_div.style.top)-window.scrollY;
553 | pwPlayer_DivX = parseInt(pwPlayer_video_div.style.left)-window.scrollX;
554 |
555 | // log("mouseY:"+_mouseY);
556 | if(inFullScreen()){
557 | // in fullscreen mode, mouse in the bottom 1/5, do nothing.
558 | if ( pwPlayer_mouseY+window.screenTop > window.outerHeight*0.8 ){
559 | return;
560 | }else{
561 | log("bingo: mouseY:" + pwPlayer_mouseY + " winTop:" + window.screenTop + " win.OH:" + window.outerHeight);
562 | // exit full screen and prepare to be deleted.
563 | exitFullscreen();
564 | document.body.removeChild(pwPlayer_divNode);
565 | return;
566 | }
567 | }
568 |
569 | // mouse is inside the pip box, but in the lower control area
570 | if (pwPlayer_mouseY > pipHeight*0.8 ) return;
571 | // Save the previous location
572 | log( "mouseY:"+ pwPlayer_mouseY + " DivY:" + pwPlayer_DivY + " divX:" + pwPlayer_DivX);
573 | // delete it.
574 | document.body.removeChild(pwPlayer_divNode);
575 | };
576 |
577 | pwPlayer_video.onended = () =>{
578 | if(inFullScreen()){
579 | // in fullscreen mode, mouse in the bottom 1/5, do nothing.
580 | if ( pwPlayer_mouseY+window.screenTop > window.outerHeight*0.8 ){
581 | return;
582 | }else{
583 | log("bingo: mouseY:" + pwPlayer_mouseY + " winTop:" + window.screenTop + " win.OH:" + window.outerHeight);
584 | // exit full screen and prepare to be deleted.
585 | exitFullscreen();
586 | document.body.removeChild(pwPlayer_divNode);
587 | return;
588 | }
589 | }
590 | document.body.removeChild(pwPlayer_divNode);
591 | };
592 |
593 | dragPiPElement(pwPlayer_video_div);
594 |
595 | }
596 |
597 |
598 | function dragPiPElement(elmnt) {
599 | var pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
600 | if (document.getElementById(elmnt.id + "header")) {
601 | // if present, the header is where you move the DIV from:
602 | document.getElementById(elmnt.id + "header").onmousedown = dragMouseDown;
603 | } else {
604 | // otherwise, move the DIV from anywhere inside the DIV:
605 | elmnt.onmousedown = dragMouseDown;
606 | }
607 |
608 | function dragMouseDown(e) {
609 | e = e || window.event;
610 | e.preventDefault();
611 | // get the mouse cursor position at startup:
612 | pos3 = e.clientX;
613 | pos4 = e.clientY;
614 | document.onmouseup = closeDragElement;
615 | // call a function whenever the cursor moves:
616 | document.onmousemove = elementDrag;
617 | }
618 |
619 | function elementDrag(e) {
620 | e = e || window.event;
621 | e.preventDefault();
622 | // calculate the new cursor position:
623 | pos1 = pos3 - e.clientX;
624 | pos2 = pos4 - e.clientY;
625 | pos3 = e.clientX;
626 | pos4 = e.clientY;
627 | // set the element's new position:
628 | elmnt.style.top = (elmnt.offsetTop - pos2) + "px";
629 | elmnt.style.left = (elmnt.offsetLeft - pos1) + "px";
630 | }
631 |
632 | function closeDragElement() {
633 | // stop moving when mouse button is released:
634 | document.onmouseup = null;
635 | document.onmousemove = null;
636 | }
637 | }
638 |
639 |
640 | function doFullScreen() {
641 | var element = document.documentElement;
642 | // Check which implementation is available
643 | var requestMethod = element.requestFullScreen ||
644 | element.webkitRequestFullScreen ||
645 | element.mozRequestFullScreen ||
646 | element.msRequestFullscreen;
647 |
648 | if( requestMethod ) {
649 | // log("fullscreen method found: " + requestMethod);
650 | requestMethod.apply( element );
651 | }else{
652 | // log("fullscreen method not found");
653 | }
654 | ;
655 |
656 | }
657 |
658 | function exitFullscreen(){
659 | var element = document;
660 | // Check which implementation is available
661 | var requestMethod = element.exitFullScreen ||
662 | element.webkitExitFullscreen ||
663 | element.mozCancelFullScreen ||
664 | element.msExitFullscreen;
665 |
666 | if( requestMethod ) {
667 | log ("have method to exit full screen." + requestMethod.toString());
668 | requestMethod.apply( element );
669 | }else{
670 | log ("no method to exit full screen.");
671 | }
672 | }
673 |
674 | function inFullScreen(){
675 | var doc = window.document;
676 | return !(!doc.fullscreenElement
677 | && !doc.mozFullScreenElement
678 | && !doc.webkitFullscreenElement
679 | && !doc.msFullscreenElement);
680 | }
--------------------------------------------------------------------------------
/pwPlayer - Oculus v0.5.js:
--------------------------------------------------------------------------------
1 | /* Inspired by clangmoyai's IINA player script in github !
2 | This script will add a "Play" button in each scene card.
3 | Allow you to easily play those video files.
4 | To use it, just copy and paste the code into Stash->Settings->Interface->Custome Javascript.
5 | Then refresh the browser.
6 |
7 | Player mode should be either "browser", "browserfull", "browserpip" or "player"
8 | * browser: The video is played within a tag. It works for most platforms.
9 | * browserfull: The video is played in the browser, but in full screen.
10 | * browserpip: The video is played in a small window in the front.
11 | * player: The browser will try to send the steam link to an external player.
12 | Player mode is still buggy, but it should work in android.
13 | A special use for browserfull mode, is to use Oculus Browser to see the content of Stash,
14 | then use "Play" to open the scene video in fullscreen quickly. It's a great way to view Stash and play scene files in Oculus Quest.
15 | Version 0.5
16 | */
17 |
18 | // settings
19 | const debug = false;
20 |
21 | const pwPlayer_mode = "browserfull";
22 |
23 | function log(str){
24 | if(debug)console.log(str);
25 | }
26 | log("program starts.");
27 |
28 | // track mouse y position
29 | var pwPlayer_mouseY = 0;
30 | document.body.onmousemove = (e) => {
31 | pwPlayer_mouseY = e.offsetY;
32 | }
33 |
34 |
35 | const pwPlayer_settings = {
36 | // Path fixes for different OS. For local only.
37 | "Windows":{
38 | // Use vlc to handle local files.
39 | "urlScheme": "vlc://",
40 | // double backsplashes need 4 backslashes.
41 | "replacePath": ["\\\\", "/"],
42 | },
43 | "Android":{
44 | // Not used.
45 | "urlScheme": "file:///",
46 | "replacePath": ["", ""],
47 | },
48 | "iOS":{
49 | // Not use
50 | "urlScheme": "file://",
51 | "replacePath": ["", ""],
52 | },
53 | "Linux":{
54 | // not use
55 | "urlScheme": "file://",
56 | "replacePath": ["", ""],
57 | },
58 | "MacOS":{
59 | // For local iina player.
60 | "urlScheme": "iina://weblink?url=file://",
61 | // Or VLC: "urlScheme": "vlc-x-callback://x-callback-url/stream?url=file://"
62 | "replacePath": ["", ""],
63 | },
64 | "Oculus":{
65 | // not use.
66 | "urlScheme": "file://",
67 | "replacePath": ["", ""],
68 | },
69 | "Others":{
70 | // not use.
71 | "urlScheme": "file://",
72 | "replacePath": ["", ""],
73 | }
74 | };
75 |
76 | // style
77 | const pwPlayer_style = document.createElement("style");
78 | pwPlayer_style.innerHTML = `
79 | .pwPlayer_button {
80 | border-radius: 3.5px;
81 | cursor: pointer;
82 | padding: 2px 9px 3px 13px;
83 | }
84 | .pwPlayer_button:hover {
85 | background-color: rgba(138, 155, 168, .15);
86 | }
87 | .pwPlayer_button svg {
88 | fill: currentColor;
89 | width: 1em;
90 | vertical-align: middle;
91 | }
92 | .pwPlayer_button span {
93 | font-size: 13px;
94 | font-weight: 500;
95 | letter-spacing: 0.1em;
96 | color: currentColor;
97 | vertical-align: middle;
98 | margin-left: 3px;
99 | }
100 | #pwPlayer_videoDiv{
101 | background: black;
102 | position: absolute;
103 | top: 0px;
104 | left: 0px;
105 | width: 100%;
106 | height: 100%;
107 | z-index: 1040;
108 | }
109 | #pwPlayer_video{
110 | object-fit: contain;
111 | object-position: center;
112 | cursor: pointer;
113 | position: relative;
114 | width: 100%;
115 | height: 100%;
116 | }
117 | #pwPlayer_videoDivPIP{
118 | background: black;
119 | position: absolute;
120 | top: 0px;
121 | left: 0px;
122 | width: 800px;
123 | height: 460px;
124 | z-index: 1040;
125 | }
126 | #pwPlayer_videoDivPIPheader{
127 | padding: 10px;
128 | cursor: move;
129 | z-index: 1040;
130 | background-color: #202124;
131 | color: #ffffff;
132 | }
133 | }
134 | `;
135 |
136 | // Only need to call once.
137 | const pwPlayer_OS = pwPlayer_getOS();
138 | log("OS: " + pwPlayer_OS);
139 |
140 | document.head.appendChild(pwPlayer_style);
141 |
142 | // api
143 | const pwPlayer_getSceneDetails = async href => {
144 | const regex = /\/scenes\/(\d+)\?/,
145 | sceneId = regex.exec(href)[1],
146 | graphQl = `
147 | {
148 | findScene(id:${sceneId}){
149 | files{
150 | path,
151 | size,
152 | format,
153 | width,
154 | height,
155 | duration,
156 | video_codec,
157 | audio_codec,
158 | frame_rate,
159 | },
160 | date,
161 | }
162 | }
163 | `,
164 | response = await fetch("/graphql", {
165 | method: "POST",
166 | headers: {
167 | "Content-Type": "application/json",
168 | },
169 | body: JSON.stringify({ query: graphQl })
170 | });
171 | return response.json();
172 | };
173 |
174 | const pwPlayer_getSceneInfo = async href => {
175 | const regex = /\/scenes\/(\d+)\?/,
176 | sceneId = regex.exec(href)[1],
177 | graphQl = `{ findScene(id: ${sceneId}) { files { path, basename }, paths{stream} } }`,
178 | response = await fetch("/graphql", {
179 | method: "POST",
180 | headers: {
181 | "Content-Type": "application/json",
182 | },
183 | body: JSON.stringify({ query: graphQl })
184 | });
185 | return response.json();
186 | };
187 |
188 |
189 | function pwPlayer_getOS() {
190 | var uA = window.navigator.userAgent,
191 | os = "Others";
192 | switch(true){
193 | case uA.includes("Win"):
194 | return "Windows";
195 | case uA.includes("Mac"):
196 | return "MacOS";
197 | case uA.includes("Oculus"):
198 | return "Oculus";
199 | case uA.includes("Linux"):
200 | return "Linux";
201 | case uA.includes("Android"):
202 | return "Android";
203 | case uA.includes("X11"):
204 | return "Unix";
205 | default:
206 | return 'Others';
207 | }
208 | }
209 |
210 | function pwPlayer_getBrowser(){
211 | // not using this much.
212 | var userAgent = window.navigator.userAgent;
213 |
214 | switch (true){
215 | case userAgent.includes("OculusBrowser"):
216 | // special detection for Quest 2.
217 | return "oculus";
218 | case userAgent.includes("chrom"):
219 | case userAgent.includes("crios"):
220 | return "chrome";
221 | case userAgent.includes("firefox"):
222 | case userAgent.includes("fxios"):
223 | return "firefox";
224 | case userAgent.includes("safari"):
225 | return "safari";
226 | case userAgent.includes("opr"):
227 | return "opera";
228 | case userAgent.includes("edg"):
229 | return "edge";
230 | default:
231 | return "others";
232 | }
233 | }
234 |
235 | const pwPlayer_config = { subtree: true, childList: true };
236 | const pwPlayer_previewElm = "video.scene-card-preview-video";
237 | // promise
238 | const pwPlayer_waitForElm = selector => {
239 | return new Promise(resolve => {
240 | if (document.querySelector(selector)) {
241 | return resolve(document.querySelector(selector));
242 | }
243 | const observer = new MutationObserver(mutations => {
244 | if (document.querySelector(selector)) {
245 | resolve(document.querySelector(selector));
246 | observer.disconnect();
247 | }
248 | });
249 | observer.observe(document.body, pwPlayer_config);
250 | });
251 | };
252 |
253 | // initial
254 | pwPlayer_waitForElm(pwPlayer_previewElm).then(() => {
255 | pwPlayer_addButton();
256 | });
257 |
258 | // route
259 | let previousUrl = "";
260 | const observer = new MutationObserver(function (mutations) {
261 | if (window.location.href !== previousUrl) {
262 | previousUrl = window.location.href;
263 | pwPlayer_waitForElm(pwPlayer_previewElm).then(() => {
264 | pwPlayer_addButton();
265 | });
266 | }
267 | });
268 |
269 | observer.observe(document, pwPlayer_config);
270 |
271 | // main
272 | const pwPlayer_addButton = () => {
273 | const scenes = document.querySelectorAll("div.row > div");
274 | for (const scene of scenes) {
275 | if (scene.querySelector("a.pwPlayer_button") != null) continue;
276 |
277 | const scene_url = scene.querySelector("a.scene-card-link"),
278 | popover = scene.querySelector("div.card-popovers"),
279 | button = document.createElement("a");
280 | button.innerHTML = `
281 |
282 |
284 | Play `;
285 |
286 | button.classList.add("pwPlayer_button");
287 | button.href = "javascript:;";
288 |
289 | button.onclick = () =>{
290 | pwPlayer_getSceneInfo(scene_url.href)
291 | .then((result) =>{
292 | const streamLink = result.data.findScene.paths.stream;
293 | const filePath = result.data.findScene.files[0].path
294 | .replace(pwPlayer_settings[pwPlayer_OS].replacePath[0],
295 | pwPlayer_settings[pwPlayer_OS].replacePath[1]);
296 |
297 |
298 | switch(pwPlayer_mode){
299 | case "browser": // normal browser mode
300 | playVideoInBrowser(streamLink);
301 | break;
302 | case "browserfull": // fullscreen browser mode
303 | playVideoInBrowser(streamLink, true);
304 | break;
305 | case "browserpip": // picture in picture browser mode
306 | playVideoPIP(streamLink);
307 | break;
308 | case "player":
309 | switch (pwPlayer_OS){
310 | case "Mac OS":
311 | // Sample local handling for iina player.
312 | // if you don't have iina player, use "remote" mode instead.
313 | if(debug)alert("playermode. you just click play in MacOS");
314 | if (pwPlayer_mode == "player"){
315 | href = pwPlayer_settings.MacOS.urlScheme +
316 | pwPlayer_settings.MacOS.replacePath[1] +
317 | encodeURIComponent(filePath);
318 | window.open(href);
319 | }
320 | break;
321 | case "Android":
322 | // Special andoid launch with intent
323 | if(debug)alert("playermode. you just click play in Android");
324 | if (button.href == "javascript:;"){
325 | url = new URL(streamLink);
326 | const scheme=url.protocol.slice(0,-1);
327 | url.hash = `Intent;action=android.intent.action.VIEW;scheme=${scheme};type=video/mp4;S.title=${encodeURI(
328 | result.data.findScene.files[0].basename
329 | )};end`;
330 | url.protocol = "intent";
331 | button.href = url.toString();
332 | button.click();
333 | }
334 | break;
335 |
336 | case "iOS":
337 | // Special ios launch
338 | if( button.href == "javascript:;"){
339 | url = new URL();
340 | url.host = "x-callback-url";
341 | url.port = "";
342 | url.pathname = "stream";
343 | url.search = `url=${encodeURIComponent(streamLink)}`;
344 | url.protocol = "vlc-x-callback";
345 | button.href = url.toString();
346 | button.click();
347 | }
348 | break;
349 | case "Oculus":
350 | // use browser built-in player
351 | if (button.href == "javascript:;"){
352 | button.href = streamLink;
353 | button.click();
354 | }
355 | break;
356 | case "Windows":
357 | if(debug)alert("playermode. you just click play in Windows");
358 | if (pwPlayer_mode == "player"){
359 | settings = pwPlayer_settings.Windows;
360 | href = settings.urlScheme + encodeURIComponent(filePath);
361 | window.open(href);
362 | }
363 | break;
364 | default:
365 | } // end of the switch about os
366 | break; // fullscreen browser mode
367 | } // end of switch of mode
368 | });
369 |
370 | }; // end of button onclick envent.
371 |
372 | if (popover) popover.append(button);
373 |
374 | button.onmouseover = () => {
375 | if (button.title.length == 0) {
376 | pwPlayer_getSceneDetails(scene_url.href)
377 | .then((result) => {
378 | // console.log("result: " + JSON.stringify(result));
379 | data = result.data.findScene;
380 | sceneFile = data.files[0];
381 | // log("before title phase.")
382 | title =`Path: ${ WrapStr(sceneFile.path,30)}
383 | Size: ${niceBytes(sceneFile.size)}
384 | Dimensions: ${sceneFile.width}x${sceneFile.height}
385 | Duration: ${toHMS(sceneFile.duration)}
386 | Codecs: ${sceneFile.video_codec}, ${sceneFile.audio_codec}
387 | Frame Rate: ${sceneFile.frame_rate}
388 | ${data.date?"Date: "+data.date : ""}`;
389 | log("title:" + title);
390 | button.title = title;
391 | });
392 | }
393 | }; // end of on mouse move over.
394 |
395 | }; // end of the each scene card loop.
396 | }; // end of pwPlayer_addButton function.
397 |
398 | function WrapStr(s,n){
399 | //
400 | if (s.length <= n) return s;
401 | str = s.substr(0,n)
402 | for (i=n;i('00'+n).slice(-2);
411 | return f(s/3600)+':'+g(f(s/60)%60)+':'+g(s%60);
412 | }
413 |
414 | function niceBytes(x){
415 | let units = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
416 | let l = 0, n = parseInt(x, 10) || 0;
417 | while(n >= 1024 && ++l){ n = n/1024; }
418 | return(n.toFixed(n < 10 && l > 0 ? 1 : 0) + ' ' + units[l]);
419 | }
420 |
421 | function playVideoInBrowser(streamLink, fullscreen = false){
422 | // It adds a video in the front of the body, while PIP adds to the end.
423 |
424 | // close previous video element, if any.
425 | const previousElm = document.body.querySelector(".pwPlayer_videoDiv");
426 | if (previousElm!==null){
427 | document.body.removeChild(previousElm);
428 | }
429 |
430 | var pwPlayer_video_div = document.createElement("div");
431 |
432 | pwPlayer_video_div.id = "pwPlayer_videoDiv";
433 | var pwPlayer_video = document.createElement("video");
434 | pwPlayer_video.id = "pwPlayer_video";
435 | pwPlayer_video.autoplay = true;
436 | pwPlayer_video.controls = true;
437 | pwPlayer_video.src = streamLink;
438 | pwPlayer_video_div.appendChild(pwPlayer_video);
439 | var pwPlayer_divNode = document.body.insertBefore(pwPlayer_video_div, document.body.firstChild);
440 | pwPlayer_video_div.width =window.innerWidth;
441 | pwPlayer_video_div.height =window.innerHeight;
442 |
443 | log("win inner w:" + window.innerWidth);
444 | log("win inner h: "+ window.innerHeight);
445 | log("div width:" + pwPlayer_video_div.width);
446 | log("div height:" + pwPlayer_video_div.height);
447 |
448 | // save the scroll postion.
449 | var pwPlayer_scrollPos;
450 | if (typeof window.pageYOffset != 'undefined') {
451 | pwPlayer_scrollPos = window.pageYOffset;
452 | }
453 | else if (typeof document.compatMode != 'undefined' && document.compatMode != 'BackCompat') {
454 | pwPlayer_scrollPos = document.documentElement.scrollTop;
455 | }
456 | else if (typeof document.body != 'undefined') {
457 | pwPlayer_scrollPos = document.body.scrollTop;
458 | }
459 | log("scroll pos:" + pwPlayer_scrollPos);
460 |
461 | window.scrollTo(0,0);
462 |
463 | // now make it full screen if enabled
464 | if (fullscreen){
465 | doFullScreen();
466 | pwPlayer_video.height = screen.height;
467 | pwPlayer_video.width = screen.width;
468 |
469 | };
470 |
471 | let pwPwPlayer_videoEnd = () =>{
472 | // all video will call this to end.
473 | // pwPlayer_video.pause(); the video usually paused already.
474 | if(inFullScreen()){
475 | exitFullscreen();
476 | }
477 | document.body.removeChild(pwPlayer_divNode);
478 | window.scrollTo(0, pwPlayer_scrollPos);
479 | };
480 |
481 | pwPlayer_video.onerror = () => {
482 | alert("Error playing this video.");
483 | log("Error in playing, scroll pos:" + pwPlayer_scrollPos);
484 | pwPwPlayer_videoEnd();
485 | };
486 |
487 | pwPlayer_video.onended = () => {
488 | // normal ending
489 | log("video reach the end.");
490 | pwPwPlayer_videoEnd();
491 | };
492 |
493 | pwPlayer_video.onpause = (event) => {
494 | if(inFullScreen()){
495 | // in fullscreen mode, mouse in the bottom 1/5, do nothing.
496 | if ( pwPlayer_mouseY+window.screenTop > window.outerHeight*0.8 ){
497 | return;
498 | }else{
499 | log("bingo: mouseY:" + pwPlayer_mouseY + " winTop:" + window.screenTop + " win.OH:" + window.outerHeight);
500 | // exit full screen and prepare to be deleted.
501 | pwPwPlayer_videoEnd();
502 | return;
503 | }
504 | }
505 | // normal video process.
506 | if ( pwPlayer_mouseY > screen.innerHeight*0.8 ) return;
507 |
508 | log("play ends, scroll pos:" + pwPlayer_scrollPos);
509 | pwPwPlayer_videoEnd();
510 | };
511 |
512 | }
513 |
514 | var pwPlayer_DivX=0, pwPlayer_DivY=0;
515 |
516 | function playVideoPIP(streamLink){
517 | // It will show a video in a dragable window
518 | const previousElm = document.body.querySelector(".pwPlayer_videoDivPIP");
519 | if (previousElm!==null){
520 | document.body.removeChild(previousElm);
521 | }
522 | var pwPlayer_video_div = document.createElement("div");
523 | pwPlayer_video_div.id = "pwPlayer_videoDivPIP";
524 | var pwPlayer_PiPHeader = document.createElement("div");
525 | pwPlayer_PiPHeader.id = "pwPlayer_videoDivPIPheader";
526 | pwPlayer_video_div.appendChild(pwPlayer_PiPHeader);
527 | var pwPlayer_video = document.createElement("video");
528 | pwPlayer_video.id = "pwPlayer_video";
529 | pwPlayer_video.autoplay = true;
530 | pwPlayer_video.controls = true;
531 | pwPlayer_video.src = streamLink;
532 | pwPlayer_video_div.appendChild(pwPlayer_video);
533 | var pwPlayer_divNode = document.body.appendChild(pwPlayer_video_div);
534 | x = (pwPlayer_DivX + pwPlayer_video_div.offsetWidth > window.innerWidth) ?
535 | window.innerWidth - pwPlayer_video_div.offsetWidth : pwPlayer_DivX ;
536 | x = (x < 0)? 0 : x;
537 | y = (pwPlayer_DivY + pwPlayer_video_div.offsetHeight> window.innerHeight)?
538 | window.innerHeight - pwPlayer_video_div.offsetHeight : pwPlayer_DivY ;
539 | y = (y < 0)? 0 : y;
540 |
541 |
542 | pwPlayer_video_div.style.top = (window.scrollY + y)+"px";
543 | pwPlayer_video_div.style.left = (window.scrollX + x)+"px";
544 |
545 | // pwPlayer_video_div.width =300;
546 | // pwPlayer_video_div.height =200;
547 |
548 | pwPlayer_video.onpause = () =>{
549 | pipWidth = pwPlayer_video_div.offsetWidth;
550 | pipHeight = pwPlayer_video_div.offsetHeight;
551 |
552 | pwPlayer_DivY = parseInt(pwPlayer_video_div.style.top)-window.scrollY;
553 | pwPlayer_DivX = parseInt(pwPlayer_video_div.style.left)-window.scrollX;
554 |
555 | // log("mouseY:"+_mouseY);
556 | if(inFullScreen()){
557 | // in fullscreen mode, mouse in the bottom 1/5, do nothing.
558 | if ( pwPlayer_mouseY+window.screenTop > window.outerHeight*0.8 ){
559 | return;
560 | }else{
561 | log("bingo: mouseY:" + pwPlayer_mouseY + " winTop:" + window.screenTop + " win.OH:" + window.outerHeight);
562 | // exit full screen and prepare to be deleted.
563 | exitFullscreen();
564 | document.body.removeChild(pwPlayer_divNode);
565 | return;
566 | }
567 | }
568 |
569 | // mouse is inside the pip box, but in the lower control area
570 | if (pwPlayer_mouseY > pipHeight*0.8 ) return;
571 | // Save the previous location
572 | log( "mouseY:"+ pwPlayer_mouseY + " DivY:" + pwPlayer_DivY + " divX:" + pwPlayer_DivX);
573 | // delete it.
574 | document.body.removeChild(pwPlayer_divNode);
575 | };
576 |
577 | pwPlayer_video.onended = () =>{
578 | if(inFullScreen()){
579 | // in fullscreen mode, mouse in the bottom 1/5, do nothing.
580 | if ( pwPlayer_mouseY+window.screenTop > window.outerHeight*0.8 ){
581 | return;
582 | }else{
583 | log("bingo: mouseY:" + pwPlayer_mouseY + " winTop:" + window.screenTop + " win.OH:" + window.outerHeight);
584 | // exit full screen and prepare to be deleted.
585 | exitFullscreen();
586 | document.body.removeChild(pwPlayer_divNode);
587 | return;
588 | }
589 | }
590 | document.body.removeChild(pwPlayer_divNode);
591 | };
592 |
593 | dragPiPElement(pwPlayer_video_div);
594 |
595 | }
596 |
597 |
598 | function dragPiPElement(elmnt) {
599 | var pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
600 | if (document.getElementById(elmnt.id + "header")) {
601 | // if present, the header is where you move the DIV from:
602 | document.getElementById(elmnt.id + "header").onmousedown = dragMouseDown;
603 | } else {
604 | // otherwise, move the DIV from anywhere inside the DIV:
605 | elmnt.onmousedown = dragMouseDown;
606 | }
607 |
608 | function dragMouseDown(e) {
609 | e = e || window.event;
610 | e.preventDefault();
611 | // get the mouse cursor position at startup:
612 | pos3 = e.clientX;
613 | pos4 = e.clientY;
614 | document.onmouseup = closeDragElement;
615 | // call a function whenever the cursor moves:
616 | document.onmousemove = elementDrag;
617 | }
618 |
619 | function elementDrag(e) {
620 | e = e || window.event;
621 | e.preventDefault();
622 | // calculate the new cursor position:
623 | pos1 = pos3 - e.clientX;
624 | pos2 = pos4 - e.clientY;
625 | pos3 = e.clientX;
626 | pos4 = e.clientY;
627 | // set the element's new position:
628 | elmnt.style.top = (elmnt.offsetTop - pos2) + "px";
629 | elmnt.style.left = (elmnt.offsetLeft - pos1) + "px";
630 | }
631 |
632 | function closeDragElement() {
633 | // stop moving when mouse button is released:
634 | document.onmouseup = null;
635 | document.onmousemove = null;
636 | }
637 | }
638 |
639 |
640 | function doFullScreen() {
641 | var element = document.documentElement;
642 | // Check which implementation is available
643 | var requestMethod = element.requestFullScreen ||
644 | element.webkitRequestFullScreen ||
645 | element.mozRequestFullScreen ||
646 | element.msRequestFullscreen;
647 |
648 | if( requestMethod ) {
649 | // log("fullscreen method found: " + requestMethod);
650 | requestMethod.apply( element );
651 | }else{
652 | // log("fullscreen method not found");
653 | }
654 | ;
655 |
656 | }
657 |
658 | function exitFullscreen(){
659 | var element = document;
660 | // Check which implementation is available
661 | var requestMethod = element.exitFullScreen ||
662 | element.webkitExitFullscreen ||
663 | element.mozCancelFullScreen ||
664 | element.msExitFullscreen;
665 |
666 | if( requestMethod ) {
667 | log ("have method to exit full screen." + requestMethod.toString());
668 | requestMethod.apply( element );
669 | }else{
670 | log ("no method to exit full screen.");
671 | }
672 | }
673 |
674 | function inFullScreen(){
675 | var doc = window.document;
676 | return !(!doc.fullscreenElement
677 | && !doc.mozFullScreenElement
678 | && !doc.webkitFullscreenElement
679 | && !doc.msFullscreenElement);
680 | }
--------------------------------------------------------------------------------
/pwPlayer - PiP v0.5.js:
--------------------------------------------------------------------------------
1 | /* Inspired by clangmoyai's IINA player script in github !
2 | This script will add a "Play" button in each scene card.
3 | Allow you to easily play those video files.
4 | * This file is pre-configured to play videos in Picture-in-Picture mode.
5 | To use it, just copy and paste the code into Stash->Settings->Interface->Custome Javascript.
6 | Then refresh the browser.
7 |
8 | Player mode should be either "browser", "browserfull", "browserpip" or "player"
9 | * browser: The video is played within a tag. It works for most platforms.
10 | * browserfull: The video is played in the browser, but in full screen.
11 | * browserpip: The video is played in a small window in the front.
12 | * player: The browser will try to send the steam link to an external player.
13 | Player mode is still buggy, but it should work in android.
14 | A special use for browserfull mode, is to use Oculus Browser to see the content of Stash,
15 | then use "Play" to open the scene video in fullscreen quickly. It's a great way to view Stash and play scene files in Oculus Quest.
16 | Version 0.5
17 | */
18 |
19 | // settings
20 | const debug = false;
21 |
22 | const pwPlayer_mode = "browserpip";
23 |
24 | function log(str){
25 | if(debug)console.log(str);
26 | }
27 | log("program starts.");
28 | log("build001");
29 | // track mouse y position
30 | var pwPlayer_mouseY = 0;
31 | document.body.onmousemove = (e) => {
32 | pwPlayer_mouseY = e.offsetY;
33 | }
34 |
35 |
36 | const pwPlayer_settings = {
37 | // Path fixes for different OS. For local only.
38 | "Windows":{
39 | // Use vlc to handle local files.
40 | "urlScheme": "vlc://",
41 | // double backsplashes need 4 backslashes.
42 | "replacePath": ["\\\\", "/"],
43 | },
44 | "Android":{
45 | // Not used.
46 | "urlScheme": "file:///",
47 | "replacePath": ["", ""],
48 | },
49 | "iOS":{
50 | // Not use
51 | "urlScheme": "file://",
52 | "replacePath": ["", ""],
53 | },
54 | "Linux":{
55 | // not use
56 | "urlScheme": "file://",
57 | "replacePath": ["", ""],
58 | },
59 | "MacOS":{
60 | // For local iina player.
61 | "urlScheme": "iina://weblink?url=file://",
62 | // Or VLC: "urlScheme": "vlc-x-callback://x-callback-url/stream?url=file://"
63 | "replacePath": ["", ""],
64 | },
65 | "Oculus":{
66 | // not use.
67 | "urlScheme": "file://",
68 | "replacePath": ["", ""],
69 | },
70 | "Others":{
71 | // not use.
72 | "urlScheme": "file://",
73 | "replacePath": ["", ""],
74 | }
75 | };
76 |
77 | // style
78 | const pwPlayer_style = document.createElement("style");
79 | pwPlayer_style.innerHTML = `
80 | .pwPlayer_button {
81 | border-radius: 3.5px;
82 | cursor: pointer;
83 | padding: 2px 9px 3px 13px;
84 | }
85 | .pwPlayer_button:hover {
86 | background-color: rgba(138, 155, 168, .15);
87 | }
88 | .pwPlayer_button svg {
89 | fill: currentColor;
90 | width: 1em;
91 | vertical-align: middle;
92 | }
93 | .pwPlayer_button span {
94 | font-size: 13px;
95 | font-weight: 500;
96 | letter-spacing: 0.1em;
97 | color: currentColor;
98 | vertical-align: middle;
99 | margin-left: 3px;
100 | }
101 | #pwPlayer_videoDiv{
102 | background: black;
103 | position: absolute;
104 | top: 0px;
105 | left: 0px;
106 | width: 100%;
107 | height: 100%;
108 | z-index: 1040;
109 | }
110 | #pwPlayer_video{
111 | object-fit: contain;
112 | object-position: center;
113 | cursor: pointer;
114 | position: relative;
115 | width: 100%;
116 | height: 100%;
117 | }
118 | #pwPlayer_videoDivPIP{
119 | background: black;
120 | position: absolute;
121 | top: 0px;
122 | left: 0px;
123 | width: 800px;
124 | height: 460px;
125 | z-index: 1040;
126 | }
127 | #pwPlayer_videoDivPIPheader{
128 | padding: 10px;
129 | cursor: move;
130 | z-index: 1040;
131 | background-color: #202124;
132 | color: #ffffff;
133 | }
134 | }
135 | `;
136 |
137 | // Only need to call once.
138 | const pwPlayer_OS = pwPlayer_getOS();
139 | log("OS: " + pwPlayer_OS);
140 |
141 | document.head.appendChild(pwPlayer_style);
142 |
143 | // api
144 | const pwPlayer_getSceneDetails = async href => {
145 | const regex = /\/scenes\/(\d+)\?/,
146 | sceneId = regex.exec(href)[1],
147 | graphQl = `
148 | {
149 | findScene(id:${sceneId}){
150 | files{
151 | path,
152 | size,
153 | format,
154 | width,
155 | height,
156 | duration,
157 | video_codec,
158 | audio_codec,
159 | frame_rate,
160 | },
161 | date,
162 | }
163 | }
164 | `,
165 | response = await fetch("/graphql", {
166 | method: "POST",
167 | headers: {
168 | "Content-Type": "application/json",
169 | },
170 | body: JSON.stringify({ query: graphQl })
171 | });
172 | return response.json();
173 | };
174 |
175 | const pwPlayer_getSceneInfo = async href => {
176 | const regex = /\/scenes\/(\d+)\?/,
177 | sceneId = regex.exec(href)[1],
178 | graphQl = `{ findScene(id: ${sceneId}) { files { path, basename }, paths{stream} } }`,
179 | response = await fetch("/graphql", {
180 | method: "POST",
181 | headers: {
182 | "Content-Type": "application/json",
183 | },
184 | body: JSON.stringify({ query: graphQl })
185 | });
186 | return response.json();
187 | };
188 |
189 |
190 | function pwPlayer_getOS() {
191 | var uA = window.navigator.userAgent,
192 | os = "Others";
193 | switch(true){
194 | case uA.includes("Win"):
195 | return "Windows";
196 | case uA.includes("Mac"):
197 | return "MacOS";
198 | case uA.includes("Oculus"):
199 | return "Oculus";
200 | case uA.includes("Linux"):
201 | return "Linux";
202 | case uA.includes("Android"):
203 | return "Android";
204 | case uA.includes("X11"):
205 | return "Unix";
206 | default:
207 | return 'Others';
208 | }
209 | }
210 |
211 | function pwPlayer_getBrowser(){
212 | // not using this much.
213 | var userAgent = window.navigator.userAgent;
214 |
215 | switch (true){
216 | case userAgent.includes("OculusBrowser"):
217 | // special detection for Quest 2.
218 | return "oculus";
219 | case userAgent.includes("chrom"):
220 | case userAgent.includes("crios"):
221 | return "chrome";
222 | case userAgent.includes("firefox"):
223 | case userAgent.includes("fxios"):
224 | return "firefox";
225 | case userAgent.includes("safari"):
226 | return "safari";
227 | case userAgent.includes("opr"):
228 | return "opera";
229 | case userAgent.includes("edg"):
230 | return "edge";
231 | default:
232 | return "others";
233 | }
234 | }
235 |
236 | const pwPlayer_config = { subtree: true, childList: true };
237 | const pwPlayer_WaitElm = "div.toast-container.row";
238 | // promise
239 | const pwPlayer_waitForElm = selector => {
240 | return new Promise(resolve => {
241 | if (document.querySelector(selector)) {
242 | return resolve(document.querySelector(selector));
243 | }
244 | const observer = new MutationObserver(mutations => {
245 | if (document.querySelector(selector)) {
246 | resolve(document.querySelector(selector));
247 | observer.disconnect();
248 | }
249 | });
250 | observer.observe(document.body, pwPlayer_config);
251 | });
252 | };
253 |
254 | // initial
255 | pwPlayer_waitForElm(pwPlayer_WaitElm).then(() => {
256 | pwPlayer_addButton();
257 | });
258 |
259 | // route
260 | let previousUrl = "";
261 | const observer = new MutationObserver(function (mutations) {
262 | if (window.location.href !== previousUrl) {
263 | previousUrl = window.location.href;
264 | pwPlayer_waitForElm(pwPlayer_previewElm).then(() => {
265 | pwPlayer_addButton();
266 | });
267 | }
268 | });
269 |
270 | observer.observe(document, pwPlayer_config);
271 |
272 | // main
273 | const pwPlayer_addButton = () => {
274 | const scenes = document.querySelectorAll("div.row > div");
275 | for (const scene of scenes) {
276 | if (scene.querySelector("a.pwPlayer_button") != null) continue;
277 |
278 | const scene_url = scene.querySelector("a.scene-card-link"),
279 | popover = scene.querySelector("div.card-popovers"),
280 | button = document.createElement("a");
281 | button.innerHTML = `
282 |
283 |
285 | Play `;
286 |
287 | button.classList.add("pwPlayer_button");
288 | button.href = "javascript:;";
289 |
290 | button.onclick = () =>{
291 | pwPlayer_getSceneInfo(scene_url.href)
292 | .then((result) =>{
293 | const streamLink = result.data.findScene.paths.stream;
294 | const filePath = result.data.findScene.files[0].path
295 | .replace(pwPlayer_settings[pwPlayer_OS].replacePath[0],
296 | pwPlayer_settings[pwPlayer_OS].replacePath[1]);
297 |
298 |
299 | switch(pwPlayer_mode){
300 | case "browser": // normal browser mode
301 | playVideoInBrowser(streamLink);
302 | break;
303 | case "browserfull": // fullscreen browser mode
304 | playVideoInBrowser(streamLink, true);
305 | break;
306 | case "browserpip": // picture in picture browser mode
307 | playVideoPIP(streamLink);
308 | break;
309 | case "player":
310 | switch (pwPlayer_OS){
311 | case "Mac OS":
312 | // Sample local handling for iina player.
313 | // if you don't have iina player, use "remote" mode instead.
314 | if(debug)alert("playermode. you just click play in MacOS");
315 | if (pwPlayer_mode == "player"){
316 | href = pwPlayer_settings.MacOS.urlScheme +
317 | pwPlayer_settings.MacOS.replacePath[1] +
318 | encodeURIComponent(filePath);
319 | window.open(href);
320 | }
321 | break;
322 | case "Android":
323 | // Special andoid launch with intent
324 | if(debug)alert("playermode. you just click play in Android");
325 | if (button.href == "javascript:;"){
326 | url = new URL(streamLink);
327 | const scheme=url.protocol.slice(0,-1);
328 | url.hash = `Intent;action=android.intent.action.VIEW;scheme=${scheme};type=video/mp4;S.title=${encodeURI(
329 | result.data.findScene.files[0].basename
330 | )};end`;
331 | url.protocol = "intent";
332 | button.href = url.toString();
333 | button.click();
334 | }
335 | break;
336 |
337 | case "iOS":
338 | // Special ios launch
339 | if( button.href == "javascript:;"){
340 | url = new URL();
341 | url.host = "x-callback-url";
342 | url.port = "";
343 | url.pathname = "stream";
344 | url.search = `url=${encodeURIComponent(streamLink)}`;
345 | url.protocol = "vlc-x-callback";
346 | button.href = url.toString();
347 | button.click();
348 | }
349 | break;
350 | case "Oculus":
351 | // use browser built-in player
352 | if (button.href == "javascript:;"){
353 | button.href = streamLink;
354 | button.click();
355 | }
356 | break;
357 | case "Windows":
358 | if(debug)alert("playermode. you just click play in Windows");
359 | if (pwPlayer_mode == "player"){
360 | settings = pwPlayer_settings.Windows;
361 | href = settings.urlScheme + encodeURIComponent(filePath);
362 | window.open(href);
363 | }
364 | break;
365 | default:
366 | } // end of the switch about os
367 | break; // fullscreen browser mode
368 | } // end of switch of mode
369 | });
370 |
371 | }; // end of button onclick envent.
372 |
373 | if (popover) popover.append(button);
374 |
375 | button.onmouseover = () => {
376 | if (button.title.length == 0) {
377 | pwPlayer_getSceneDetails(scene_url.href)
378 | .then((result) => {
379 | // console.log("result: " + JSON.stringify(result));
380 | data = result.data.findScene;
381 | sceneFile = data.files[0];
382 | // log("before title phase.")
383 | title =`Path: ${ WrapStr(sceneFile.path,30)}
384 | Size: ${niceBytes(sceneFile.size)}
385 | Dimensions: ${sceneFile.width}x${sceneFile.height}
386 | Duration: ${toHMS(sceneFile.duration)}
387 | Codecs: ${sceneFile.video_codec}, ${sceneFile.audio_codec}
388 | Frame Rate: ${sceneFile.frame_rate}
389 | ${data.date?"Date: "+data.date : ""}`;
390 | log("title:" + title);
391 | button.title = title;
392 | });
393 | }
394 | }; // end of on mouse move over.
395 |
396 | }; // end of the each scene card loop.
397 | }; // end of pwPlayer_addButton function.
398 |
399 | function WrapStr(s,n){
400 | //
401 | if (s.length <= n) return s;
402 | str = s.substr(0,n)
403 | for (i=n;i('00'+n).slice(-2);
412 | return f(s/3600)+':'+g(f(s/60)%60)+':'+g(s%60);
413 | }
414 |
415 | function niceBytes(x){
416 | let units = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
417 | let l = 0, n = parseInt(x, 10) || 0;
418 | while(n >= 1024 && ++l){ n = n/1024; }
419 | return(n.toFixed(n < 10 && l > 0 ? 1 : 0) + ' ' + units[l]);
420 | }
421 |
422 | function playVideoInBrowser(streamLink, fullscreen = false){
423 | // It adds a video in the front of the body, while PIP adds to the end.
424 |
425 | // close previous video element, if any.
426 | const previousElm = document.body.querySelector(".pwPlayer_videoDiv");
427 | if (previousElm!==null){
428 | document.body.removeChild(previousElm);
429 | }
430 |
431 | var pwPlayer_video_div = document.createElement("div");
432 |
433 | pwPlayer_video_div.id = "pwPlayer_videoDiv";
434 | var pwPlayer_video = document.createElement("video");
435 | pwPlayer_video.id = "pwPlayer_video";
436 | pwPlayer_video.autoplay = true;
437 | pwPlayer_video.controls = true;
438 | pwPlayer_video.src = streamLink;
439 | pwPlayer_video_div.appendChild(pwPlayer_video);
440 | var pwPlayer_divNode = document.body.insertBefore(pwPlayer_video_div, document.body.firstChild);
441 | pwPlayer_video_div.width =window.innerWidth;
442 | pwPlayer_video_div.height =window.innerHeight;
443 |
444 | log("win inner w:" + window.innerWidth);
445 | log("win inner h: "+ window.innerHeight);
446 | log("div width:" + pwPlayer_video_div.width);
447 | log("div height:" + pwPlayer_video_div.height);
448 |
449 | // save the scroll postion.
450 | var pwPlayer_scrollPos;
451 | if (typeof window.pageYOffset != 'undefined') {
452 | pwPlayer_scrollPos = window.pageYOffset;
453 | }
454 | else if (typeof document.compatMode != 'undefined' && document.compatMode != 'BackCompat') {
455 | pwPlayer_scrollPos = document.documentElement.scrollTop;
456 | }
457 | else if (typeof document.body != 'undefined') {
458 | pwPlayer_scrollPos = document.body.scrollTop;
459 | }
460 | log("scroll pos:" + pwPlayer_scrollPos);
461 |
462 | window.scrollTo(0,0);
463 |
464 | // now make it full screen if enabled
465 | if (fullscreen){
466 | doFullScreen();
467 | pwPlayer_video.height = screen.height;
468 | pwPlayer_video.width = screen.width;
469 |
470 | };
471 |
472 | let pwPwPlayer_videoEnd = () =>{
473 | // all video will call this to end.
474 | // pwPlayer_video.pause(); the video usually paused already.
475 | if(inFullScreen()){
476 | exitFullscreen();
477 | }
478 | document.body.removeChild(pwPlayer_divNode);
479 | window.scrollTo(0, pwPlayer_scrollPos);
480 | };
481 |
482 | pwPlayer_video.onerror = () => {
483 | alert("Error playing this video.");
484 | log("Error in playing, scroll pos:" + pwPlayer_scrollPos);
485 | pwPwPlayer_videoEnd();
486 | };
487 |
488 | pwPlayer_video.onended = () => {
489 | // normal ending
490 | log("video reach the end.");
491 | pwPwPlayer_videoEnd();
492 | };
493 |
494 | pwPlayer_video.onpause = (event) => {
495 | if(inFullScreen()){
496 | // in fullscreen mode, mouse in the bottom 1/5, do nothing.
497 | if ( pwPlayer_mouseY+window.screenTop > window.outerHeight*0.8 ){
498 | return;
499 | }else{
500 | log("bingo: mouseY:" + pwPlayer_mouseY + " winTop:" + window.screenTop + " win.OH:" + window.outerHeight);
501 | // exit full screen and prepare to be deleted.
502 | pwPwPlayer_videoEnd();
503 | return;
504 | }
505 | }
506 | // normal video process.
507 | if ( pwPlayer_mouseY > screen.innerHeight*0.8 ) return;
508 |
509 | log("play ends, scroll pos:" + pwPlayer_scrollPos);
510 | pwPwPlayer_videoEnd();
511 | };
512 |
513 | }
514 |
515 | var pwPlayer_DivX=0, pwPlayer_DivY=0;
516 |
517 | function playVideoPIP(streamLink){
518 | // It will show a video in a dragable window
519 | const previousElm = document.body.querySelector(".pwPlayer_videoDivPIP");
520 | if (previousElm!==null){
521 | document.body.removeChild(previousElm);
522 | }
523 | var pwPlayer_video_div = document.createElement("div");
524 | pwPlayer_video_div.id = "pwPlayer_videoDivPIP";
525 | var pwPlayer_PiPHeader = document.createElement("div");
526 | pwPlayer_PiPHeader.id = "pwPlayer_videoDivPIPheader";
527 | pwPlayer_video_div.appendChild(pwPlayer_PiPHeader);
528 | var pwPlayer_video = document.createElement("video");
529 | pwPlayer_video.id = "pwPlayer_video";
530 | pwPlayer_video.autoplay = true;
531 | pwPlayer_video.controls = true;
532 | pwPlayer_video.src = streamLink;
533 | pwPlayer_video_div.appendChild(pwPlayer_video);
534 | var pwPlayer_divNode = document.body.appendChild(pwPlayer_video_div);
535 | x = (pwPlayer_DivX + pwPlayer_video_div.offsetWidth > window.innerWidth) ?
536 | window.innerWidth - pwPlayer_video_div.offsetWidth : pwPlayer_DivX ;
537 | x = (x < 0)? 0 : x;
538 | y = (pwPlayer_DivY + pwPlayer_video_div.offsetHeight> window.innerHeight)?
539 | window.innerHeight - pwPlayer_video_div.offsetHeight : pwPlayer_DivY ;
540 | y = (y < 0)? 0 : y;
541 |
542 |
543 | pwPlayer_video_div.style.top = (window.scrollY + y)+"px";
544 | pwPlayer_video_div.style.left = (window.scrollX + x)+"px";
545 |
546 | // pwPlayer_video_div.width =300;
547 | // pwPlayer_video_div.height =200;
548 |
549 | pwPlayer_video.onpause = () =>{
550 | pipWidth = pwPlayer_video_div.offsetWidth;
551 | pipHeight = pwPlayer_video_div.offsetHeight;
552 |
553 | pwPlayer_DivY = parseInt(pwPlayer_video_div.style.top)-window.scrollY;
554 | pwPlayer_DivX = parseInt(pwPlayer_video_div.style.left)-window.scrollX;
555 |
556 | // log("mouseY:"+_mouseY);
557 | if(inFullScreen()){
558 | // in fullscreen mode, mouse in the bottom 1/5, do nothing.
559 | if ( pwPlayer_mouseY+window.screenTop > window.outerHeight*0.8 ){
560 | return;
561 | }else{
562 | log("bingo: mouseY:" + pwPlayer_mouseY + " winTop:" + window.screenTop + " win.OH:" + window.outerHeight);
563 | // exit full screen and prepare to be deleted.
564 | exitFullscreen();
565 | document.body.removeChild(pwPlayer_divNode);
566 | return;
567 | }
568 | }
569 |
570 | // mouse is inside the pip box, but in the lower control area
571 | if (pwPlayer_mouseY > pipHeight*0.8 ) return;
572 | // Save the previous location
573 | log( "mouseY:"+ pwPlayer_mouseY + " DivY:" + pwPlayer_DivY + " divX:" + pwPlayer_DivX);
574 | // delete it.
575 | document.body.removeChild(pwPlayer_divNode);
576 | };
577 |
578 | pwPlayer_video.onended = () =>{
579 | if(inFullScreen()){
580 | // in fullscreen mode, mouse in the bottom 1/5, do nothing.
581 | if ( pwPlayer_mouseY+window.screenTop > window.outerHeight*0.8 ){
582 | return;
583 | }else{
584 | log("bingo: mouseY:" + pwPlayer_mouseY + " winTop:" + window.screenTop + " win.OH:" + window.outerHeight);
585 | // exit full screen and prepare to be deleted.
586 | exitFullscreen();
587 | document.body.removeChild(pwPlayer_divNode);
588 | return;
589 | }
590 | }
591 | document.body.removeChild(pwPlayer_divNode);
592 | };
593 |
594 | dragPiPElement(pwPlayer_video_div);
595 |
596 | }
597 |
598 |
599 | function dragPiPElement(elmnt) {
600 | var pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
601 | if (document.getElementById(elmnt.id + "header")) {
602 | // if present, the header is where you move the DIV from:
603 | document.getElementById(elmnt.id + "header").onmousedown = dragMouseDown;
604 | } else {
605 | // otherwise, move the DIV from anywhere inside the DIV:
606 | elmnt.onmousedown = dragMouseDown;
607 | }
608 |
609 | function dragMouseDown(e) {
610 | e = e || window.event;
611 | e.preventDefault();
612 | // get the mouse cursor position at startup:
613 | pos3 = e.clientX;
614 | pos4 = e.clientY;
615 | document.onmouseup = closeDragElement;
616 | // call a function whenever the cursor moves:
617 | document.onmousemove = elementDrag;
618 | }
619 |
620 | function elementDrag(e) {
621 | e = e || window.event;
622 | e.preventDefault();
623 | // calculate the new cursor position:
624 | pos1 = pos3 - e.clientX;
625 | pos2 = pos4 - e.clientY;
626 | pos3 = e.clientX;
627 | pos4 = e.clientY;
628 | // set the element's new position:
629 | elmnt.style.top = (elmnt.offsetTop - pos2) + "px";
630 | elmnt.style.left = (elmnt.offsetLeft - pos1) + "px";
631 | }
632 |
633 | function closeDragElement() {
634 | // stop moving when mouse button is released:
635 | document.onmouseup = null;
636 | document.onmousemove = null;
637 | }
638 | }
639 |
640 |
641 | function doFullScreen() {
642 | var element = document.documentElement;
643 | // Check which implementation is available
644 | var requestMethod = element.requestFullScreen ||
645 | element.webkitRequestFullScreen ||
646 | element.mozRequestFullScreen ||
647 | element.msRequestFullscreen;
648 |
649 | if( requestMethod ) {
650 | // log("fullscreen method found: " + requestMethod);
651 | requestMethod.apply( element );
652 | }else{
653 | // log("fullscreen method not found");
654 | }
655 | ;
656 |
657 | }
658 |
659 | function exitFullscreen(){
660 | var element = document;
661 | // Check which implementation is available
662 | var requestMethod = element.exitFullScreen ||
663 | element.webkitExitFullscreen ||
664 | element.mozCancelFullScreen ||
665 | element.msExitFullscreen;
666 |
667 | if( requestMethod ) {
668 | log ("have method to exit full screen." + requestMethod.toString());
669 | requestMethod.apply( element );
670 | }else{
671 | log ("no method to exit full screen.");
672 | }
673 | }
674 |
675 | function inFullScreen(){
676 | var doc = window.document;
677 | return !(!doc.fullscreenElement
678 | && !doc.mozFullScreenElement
679 | && !doc.webkitFullscreenElement
680 | && !doc.msFullscreenElement);
681 | }
--------------------------------------------------------------------------------
/pwPlayer.js:
--------------------------------------------------------------------------------
1 | /* Inspired by clangmoyai's IINA player script in github !
2 | This script will add a "Play" button in each scene card.
3 | Allow you to easily play those video files.
4 | To use it, just copy and paste the code into Stash->Settings->Interface->Custome Javascript.
5 | Then refresh the browser.
6 |
7 | Player mode should be either "browser", "browserfull", "browserpip" or "player"
8 | * browser: The video is played within a tag. It works for most platforms.
9 | * browserfull: The video is played in the browser, but in full screen.
10 | * browserpip: The video is played in a small window in the front.
11 | * player: The browser will try to send the steam link to an external player.
12 | Player mode is still buggy, but it should work in android.
13 | A special use for browserfull mode, is to use Oculus Browser to see the content of Stash,
14 | then use "Play" to open the scene video in fullscreen quickly. It's a great way to view Stash and play scene files in Oculus Quest.
15 | Version 0.6
16 | */
17 |
18 | // settings
19 | const debug = true;
20 |
21 | const pwPlayer_mode = "browserpip";
22 |
23 | function log(str){
24 | if(debug)console.log(str);
25 | }
26 | log("program starts.");
27 | log("build001");
28 | // track mouse y position
29 | var pwPlayer_mouseY = 0;
30 | document.body.onmousemove = (e) => {
31 | pwPlayer_mouseY = e.offsetY;
32 | }
33 |
34 | const pwPlayer_settings = {
35 | // Path fixes for different OS. For local only.
36 | "Windows":{
37 | // Use vlc to handle local files.
38 | "urlScheme": "vlc://",
39 | // double backsplashes need 4 backslashes.
40 | "replacePath": ["\\\\", "/"],
41 | },
42 | "Android":{
43 | // Not used.
44 | "urlScheme": "file:///",
45 | "replacePath": ["", ""],
46 | },
47 | "iOS":{
48 | // Not use
49 | "urlScheme": "file://",
50 | "replacePath": ["", ""],
51 | },
52 | "Linux":{
53 | // not use
54 | "urlScheme": "file://",
55 | "replacePath": ["", ""],
56 | },
57 | "MacOS":{
58 | // For local iina player.
59 | "urlScheme": "iina://weblink?url=file://",
60 | // Or VLC: "urlScheme": "vlc-x-callback://x-callback-url/stream?url=file://"
61 | "replacePath": ["", ""],
62 | },
63 | "Oculus":{
64 | // not use.
65 | "urlScheme": "file://",
66 | "replacePath": ["", ""],
67 | },
68 | "Others":{
69 | // not use.
70 | "urlScheme": "file://",
71 | "replacePath": ["", ""],
72 | }
73 | };
74 |
75 | // style
76 | const pwPlayer_style = document.createElement("style");
77 | pwPlayer_style.innerHTML = `
78 | .pwPlayer_button {
79 | border-radius: 3.5px;
80 | cursor: pointer;
81 | padding: 2px 9px 3px 13px;
82 | }
83 | .pwPlayer_button:hover {
84 | background-color: rgba(138, 155, 168, .15);
85 | }
86 | .pwPlayer_button svg {
87 | fill: currentColor;
88 | width: 1em;
89 | vertical-align: middle;
90 | }
91 | .pwPlayer_button span {
92 | font-size: 13px;
93 | font-weight: 500;
94 | letter-spacing: 0.1em;
95 | color: currentColor;
96 | vertical-align: middle;
97 | margin-left: 3px;
98 | }
99 | #pwPlayer_videoDiv{
100 | background: black;
101 | position: absolute;
102 | top: 0px;
103 | left: 0px;
104 | width: 100%;
105 | height: 100%;
106 | z-index: 1040;
107 | }
108 | #pwPlayer_video{
109 | object-fit: contain;
110 | object-position: center;
111 | cursor: pointer;
112 | position: relative;
113 | width: 100%;
114 | height: 100%;
115 | }
116 | #pwPlayer_videoDivPIP{
117 | background: black;
118 | position: absolute;
119 | top: 0px;
120 | left: 0px;
121 | width: 800px;
122 | height: 460px;
123 | z-index: 1040;
124 | }
125 | #pwPlayer_videoDivPIPheader{
126 | padding: 10px;
127 | cursor: move;
128 | z-index: 1040;
129 | background-color: #202124;
130 | color: #ffffff;
131 | }
132 | }
133 | `;
134 |
135 | // Only need to call once.
136 | const pwPlayer_OS = pwPlayer_getOS();
137 | log("OS: " + pwPlayer_OS);
138 |
139 | var pwPlayer_styleNode = document.head.appendChild(pwPlayer_style);
140 |
141 | // api
142 | const pwPlayer_getSceneDetails = async href => {
143 | const regex = /\/scenes\/(\d+)\?/,
144 | sceneId = regex.exec(href)[1],
145 | graphQl = `
146 | {
147 | findScene(id:${sceneId}){
148 | files{
149 | path,
150 | size,
151 | format,
152 | width,
153 | height,
154 | duration,
155 | video_codec,
156 | audio_codec,
157 | frame_rate,
158 | },
159 | date,
160 | }
161 | }
162 | `,
163 | response = await fetch("/graphql", {
164 | method: "POST",
165 | headers: {
166 | "Content-Type": "application/json",
167 | },
168 | body: JSON.stringify({ query: graphQl })
169 | });
170 | return response.json();
171 | };
172 |
173 | const pwPlayer_getSceneInfo = async href => {
174 | const regex = /\/scenes\/(\d+)\?/,
175 | sceneId = regex.exec(href)[1],
176 | graphQl = `{ findScene(id: ${sceneId}) { files { path, basename }, paths{stream} } }`,
177 | response = await fetch("/graphql", {
178 | method: "POST",
179 | headers: {
180 | "Content-Type": "application/json",
181 | },
182 | body: JSON.stringify({ query: graphQl })
183 | });
184 | return response.json();
185 | };
186 |
187 |
188 | function pwPlayer_getOS() {
189 | var uA = window.navigator.userAgent,
190 | os = "Others";
191 | switch(true){
192 | case uA.includes("Win"):
193 | return "Windows";
194 | case uA.includes("Mac"):
195 | return "MacOS";
196 | case uA.includes("Oculus"):
197 | return "Oculus";
198 | case uA.includes("Linux"):
199 | return "Linux";
200 | case uA.includes("Android"):
201 | return "Android";
202 | case uA.includes("X11"):
203 | return "Unix";
204 | default:
205 | return 'Others';
206 | }
207 | }
208 |
209 | function pwPlayer_getBrowser(){
210 | // not using this much.
211 | var userAgent = window.navigator.userAgent;
212 |
213 | switch (true){
214 | case userAgent.includes("OculusBrowser"):
215 | // special detection for Quest 2.
216 | return "oculus";
217 | case userAgent.includes("chrom"):
218 | case userAgent.includes("crios"):
219 | return "chrome";
220 | case userAgent.includes("firefox"):
221 | case userAgent.includes("fxios"):
222 | return "firefox";
223 | case userAgent.includes("safari"):
224 | return "safari";
225 | case userAgent.includes("opr"):
226 | return "opera";
227 | case userAgent.includes("edg"):
228 | return "edge";
229 | default:
230 | return "others";
231 | }
232 | }
233 |
234 | const pwPlayer_config = { subtree: true, childList: true };
235 | const pwPlayer_WaitElm = "video.scene-card-preview-video";
236 | // promise
237 | const pwPlayer_waitForElm = selector => {
238 | return new Promise(resolve => {
239 | if (document.querySelector(selector)) {
240 | return resolve(document.querySelector(selector));
241 | }
242 | const observer = new MutationObserver(mutations => {
243 | if (document.querySelector(selector)) {
244 | resolve(document.querySelector(selector));
245 | observer.disconnect();
246 | }
247 | });
248 | observer.observe(document.body, pwPlayer_config);
249 | });
250 | };
251 |
252 | // initial start point
253 | /*
254 | pwPlayer_waitForElm(pwPlayer_WaitElm).then(() => {
255 | pwPlayer_addButton();
256 | });
257 | */
258 | // route
259 | let previousUrl = "";
260 | const observer = new MutationObserver(function (mutations) {
261 | if (window.location.href !== previousUrl) {
262 | previousUrl = window.location.href;
263 | pwPlayer_waitForElm(pwPlayer_WaitElm).then(() => {
264 | pwPlayer_addButton();
265 | });
266 | }
267 | });
268 |
269 | observer.observe(document.body, pwPlayer_config);
270 |
271 | // main
272 | const pwPlayer_addButton = () => {
273 | const cat = pwPlayer_getCat();
274 | if (cat =="root"){
275 | css = "div.slick-track > div";
276 | }else{
277 | css = "div.scene-card ";
278 | }
279 |
280 | const scenes = document.querySelectorAll(css);
281 |
282 | for (const scene of scenes) {
283 | if (scene.querySelector("a.pwPlayer_button") != null) continue;
284 |
285 | const scene_url = scene.querySelector("a.scene-card-link");
286 | if (scene_url===null) continue;
287 | const popover = scene.querySelector("div.card-popovers"),
288 | button = document.createElement("a");
289 | button.innerHTML = `
290 |
291 |
293 | Play `;
294 |
295 | button.classList.add("pwPlayer_button");
296 | button.href = "javascript:;";
297 |
298 | button.onclick = () =>{
299 | pwPlayer_getSceneInfo(scene_url.href)
300 | .then((result) =>{
301 | const streamLink = result.data.findScene.paths.stream;
302 | const filePath = result.data.findScene.files[0].path
303 | .replace(pwPlayer_settings[pwPlayer_OS].replacePath[0],
304 | pwPlayer_settings[pwPlayer_OS].replacePath[1]);
305 |
306 |
307 | switch(pwPlayer_mode){
308 | case "browser": // normal browser mode
309 | playVideoInBrowser(streamLink);
310 | break;
311 | case "browserfull": // fullscreen browser mode
312 | playVideoInBrowser(streamLink, true);
313 | break;
314 | case "browserpip": // picture in picture browser mode
315 | playVideoPIP(streamLink);
316 | break;
317 | case "player":
318 | switch (pwPlayer_OS){
319 | case "Mac OS":
320 | // Sample local handling for iina player.
321 | // if you don't have iina player, use "remote" mode instead.
322 | if(debug)alert("playermode. you just click play in MacOS");
323 | if (pwPlayer_mode == "player"){
324 | href = pwPlayer_settings.MacOS.urlScheme +
325 | pwPlayer_settings.MacOS.replacePath[1] +
326 | encodeURIComponent(filePath);
327 | window.open(href);
328 | }
329 | break;
330 | case "Android":
331 | // Special andoid launch with intent
332 | if(debug)alert("playermode. you just click play in Android");
333 | if (button.href == "javascript:;"){
334 | url = new URL(streamLink);
335 | const scheme=url.protocol.slice(0,-1);
336 | url.hash = `Intent;action=android.intent.action.VIEW;scheme=${scheme};type=video/mp4;S.title=${encodeURI(
337 | result.data.findScene.files[0].basename
338 | )};end`;
339 | url.protocol = "intent";
340 | button.href = url.toString();
341 | button.click();
342 | }
343 | break;
344 |
345 | case "iOS":
346 | // Special ios launch
347 | if( button.href == "javascript:;"){
348 | url = new URL();
349 | url.host = "x-callback-url";
350 | url.port = "";
351 | url.pathname = "stream";
352 | url.search = `url=${encodeURIComponent(streamLink)}`;
353 | url.protocol = "vlc-x-callback";
354 | button.href = url.toString();
355 | button.click();
356 | }
357 | break;
358 | case "Oculus":
359 | // use browser built-in player
360 | if (button.href == "javascript:;"){
361 | button.href = streamLink;
362 | button.click();
363 | }
364 | break;
365 | case "Windows":
366 | if(debug)alert("playermode. you just click play in Windows");
367 | if (pwPlayer_mode == "player"){
368 | settings = pwPlayer_settings.Windows;
369 | href = settings.urlScheme + encodeURIComponent(filePath);
370 | window.open(href);
371 | }
372 | break;
373 | default:
374 | } // end of the switch about os
375 | break; // fullscreen browser mode
376 | } // end of switch of mode
377 | });
378 |
379 | }; // end of button onclick envent.
380 |
381 | if (popover) popover.append(button);
382 |
383 | button.onmouseover = () => {
384 | if (button.title.length == 0) {
385 | pwPlayer_getSceneDetails(scene_url.href)
386 | .then((result) => {
387 | // console.log("result: " + JSON.stringify(result));
388 | data = result.data.findScene;
389 | sceneFile = data.files[0];
390 | // log("before title phase.")
391 | title =`Path: ${ WrapStr(sceneFile.path,30)}
392 | Size: ${niceBytes(sceneFile.size)}
393 | Dimensions: ${sceneFile.width}x${sceneFile.height}
394 | Duration: ${toHMS(sceneFile.duration)}
395 | Codecs: ${sceneFile.video_codec}, ${sceneFile.audio_codec}
396 | Frame Rate: ${sceneFile.frame_rate}
397 | ${data.date?"Date: "+data.date : ""}`;
398 | log("title:" + title);
399 | button.title = title;
400 | });
401 | }
402 | }; // end of on mouse move over.
403 |
404 | }; // end of the each scene card loop.
405 | }; // end of pwPlayer_addButton function.
406 |
407 | // helper functions
408 |
409 | function pwPlayer_getCat(){
410 | url = new URL(window.location.href);
411 | path = String(url.pathname);
412 | log("path:" + path);
413 | if(path == "/" || path == "") return "root";
414 | const catArray = path.match( /\/[a-z]+/ )
415 | if (catArray == null ) return "others";
416 | if(catArray[0] == "/galleries") {
417 | return "gallery";
418 | }else{
419 | // get rid of the '/' in the beginning and "s" in the end
420 | cat = catArray[0].slice(1,-1);
421 | if (["scene", "movie", "image", "performer" ].indexOf(cat) !== -1)
422 | return cat;
423 | return "others";
424 | }
425 | }
426 |
427 | function WrapStr(s,n){
428 | //
429 | if (s.length <= n) return s;
430 | str = s.substr(0,n)
431 | for (i=n;i('00'+n).slice(-2);
440 | return f(s/3600)+':'+g(f(s/60)%60)+':'+g(s%60);
441 | }
442 |
443 | function niceBytes(x){
444 | let units = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
445 | let l = 0, n = parseInt(x, 10) || 0;
446 | while(n >= 1024 && ++l){ n = n/1024; }
447 | return(n.toFixed(n < 10 && l > 0 ? 1 : 0) + ' ' + units[l]);
448 | }
449 |
450 | function playVideoInBrowser(streamLink, fullscreen = false){
451 | // It adds a video in the front of the body, while PIP adds to the end.
452 |
453 | // close previous video element, if any.
454 | const previousElm = document.body.querySelector(".pwPlayer_videoDiv");
455 | if (previousElm!==null){
456 | document.body.removeChild(previousElm);
457 | }
458 |
459 | var pwPlayer_video_div = document.createElement("div");
460 |
461 | pwPlayer_video_div.id = "pwPlayer_videoDiv";
462 | var pwPlayer_video = document.createElement("video");
463 | pwPlayer_video.id = "pwPlayer_video";
464 | pwPlayer_video.autoplay = true;
465 | pwPlayer_video.controls = true;
466 | pwPlayer_video.src = streamLink;
467 | pwPlayer_video_div.appendChild(pwPlayer_video);
468 | var pwPlayer_divNode = document.body.insertBefore(pwPlayer_video_div, document.body.firstChild);
469 | pwPlayer_video_div.width =window.innerWidth;
470 | pwPlayer_video_div.height =window.innerHeight;
471 |
472 | log("win inner w:" + window.innerWidth);
473 | log("win inner h: "+ window.innerHeight);
474 | log("div width:" + pwPlayer_video_div.width);
475 | log("div height:" + pwPlayer_video_div.height);
476 |
477 | // save the scroll postion.
478 | var pwPlayer_scrollPos;
479 | if (typeof window.pageYOffset != 'undefined') {
480 | pwPlayer_scrollPos = window.pageYOffset;
481 | }
482 | else if (typeof document.compatMode != 'undefined' && document.compatMode != 'BackCompat') {
483 | pwPlayer_scrollPos = document.documentElement.scrollTop;
484 | }
485 | else if (typeof document.body != 'undefined') {
486 | pwPlayer_scrollPos = document.body.scrollTop;
487 | }
488 | log("scroll pos:" + pwPlayer_scrollPos);
489 |
490 | window.scrollTo(0,0);
491 |
492 | // now make it full screen if enabled
493 | if (fullscreen){
494 | doFullScreen();
495 | pwPlayer_video.height = screen.height;
496 | pwPlayer_video.width = screen.width;
497 |
498 | };
499 |
500 | let pwPwPlayer_videoEnd = () =>{
501 | // all video will call this to end.
502 | // pwPlayer_video.pause(); the video usually paused already.
503 | if(inFullScreen()){
504 | exitFullscreen();
505 | }
506 | document.body.removeChild(pwPlayer_divNode);
507 | window.scrollTo(0, pwPlayer_scrollPos);
508 | };
509 |
510 | pwPlayer_video.onerror = () => {
511 | alert("Error playing this video.");
512 | log("Error in playing, scroll pos:" + pwPlayer_scrollPos);
513 | pwPwPlayer_videoEnd();
514 | };
515 |
516 | pwPlayer_video.onended = () => {
517 | // normal ending
518 | log("video reach the end.");
519 | pwPwPlayer_videoEnd();
520 | };
521 |
522 | pwPlayer_video.onpause = (event) => {
523 | if(inFullScreen()){
524 | // in fullscreen mode, mouse in the bottom 1/5, do nothing.
525 | if ( pwPlayer_mouseY+window.screenTop > window.outerHeight*0.8 ){
526 | return;
527 | }else{
528 | log("bingo: mouseY:" + pwPlayer_mouseY + " winTop:" + window.screenTop + " win.OH:" + window.outerHeight);
529 | // exit full screen and prepare to be deleted.
530 | pwPwPlayer_videoEnd();
531 | return;
532 | }
533 | }
534 | // normal video process.
535 | if ( pwPlayer_mouseY > screen.innerHeight*0.8 ) return;
536 |
537 | log("play ends, scroll pos:" + pwPlayer_scrollPos);
538 | pwPwPlayer_videoEnd();
539 | };
540 |
541 | }
542 |
543 | var pwPlayer_DivX=0, pwPlayer_DivY=0;
544 |
545 | function playVideoPIP(streamLink){
546 | // It will show a video in a dragable window
547 | const previousElm = document.body.querySelector(".pwPlayer_videoDivPIP");
548 | if (previousElm!==null){
549 | document.body.removeChild(previousElm);
550 | }
551 | var pwPlayer_video_div = document.createElement("div");
552 | pwPlayer_video_div.id = "pwPlayer_videoDivPIP";
553 | var pwPlayer_PiPHeader = document.createElement("div");
554 | pwPlayer_PiPHeader.id = "pwPlayer_videoDivPIPheader";
555 | pwPlayer_video_div.appendChild(pwPlayer_PiPHeader);
556 | var pwPlayer_video = document.createElement("video");
557 | pwPlayer_video.id = "pwPlayer_video";
558 | pwPlayer_video.autoplay = true;
559 | pwPlayer_video.controls = true;
560 | pwPlayer_video.src = streamLink;
561 | pwPlayer_video_div.appendChild(pwPlayer_video);
562 | var pwPlayer_divNode = document.body.appendChild(pwPlayer_video_div);
563 | x = (pwPlayer_DivX + pwPlayer_video_div.offsetWidth > window.innerWidth) ?
564 | window.innerWidth - pwPlayer_video_div.offsetWidth : pwPlayer_DivX ;
565 | x = (x < 0)? 0 : x;
566 | y = (pwPlayer_DivY + pwPlayer_video_div.offsetHeight> window.innerHeight)?
567 | window.innerHeight - pwPlayer_video_div.offsetHeight : pwPlayer_DivY ;
568 | y = (y < 0)? 0 : y;
569 |
570 |
571 | pwPlayer_video_div.style.top = (window.scrollY + y)+"px";
572 | pwPlayer_video_div.style.left = (window.scrollX + x)+"px";
573 |
574 | // pwPlayer_video_div.width =300;
575 | // pwPlayer_video_div.height =200;
576 | pwPlayer_video.onerror = () =>{
577 | document.body.removeChild(pwPlayer_divNode);
578 | alert("Error Playing this video.");
579 | }
580 |
581 | pwPlayer_video.onpause = () =>{
582 | pipWidth = pwPlayer_video_div.offsetWidth;
583 | pipHeight = pwPlayer_video_div.offsetHeight;
584 |
585 | pwPlayer_DivY = parseInt(pwPlayer_video_div.style.top)-window.scrollY;
586 | pwPlayer_DivX = parseInt(pwPlayer_video_div.style.left)-window.scrollX;
587 |
588 | // log("mouseY:"+_mouseY);
589 | if(inFullScreen()){
590 | // in fullscreen mode, mouse in the bottom 1/5, do nothing.
591 | if ( pwPlayer_mouseY+window.screenTop > window.outerHeight*0.8 ){
592 | return;
593 | }else{
594 | log("bingo: mouseY:" + pwPlayer_mouseY + " winTop:" + window.screenTop + " win.OH:" + window.outerHeight);
595 | // exit full screen and prepare to be deleted.
596 | exitFullscreen();
597 | document.body.removeChild(pwPlayer_divNode);
598 | return;
599 | }
600 | }
601 |
602 | // mouse is inside the pip box, but in the lower control area
603 | if (pwPlayer_mouseY > pipHeight*0.8 ) return;
604 | // Save the previous location
605 | log( "mouseY:"+ pwPlayer_mouseY + " DivY:" + pwPlayer_DivY + " divX:" + pwPlayer_DivX);
606 | // delete it.
607 | document.body.removeChild(pwPlayer_divNode);
608 | };
609 |
610 | pwPlayer_video.onended = () =>{
611 | if(inFullScreen()){
612 | // in fullscreen mode, mouse in the bottom 1/5, do nothing.
613 | if ( pwPlayer_mouseY+window.screenTop > window.outerHeight*0.8 ){
614 | return;
615 | }else{
616 | log("bingo: mouseY:" + pwPlayer_mouseY + " winTop:" + window.screenTop + " win.OH:" + window.outerHeight);
617 | // exit full screen and prepare to be deleted.
618 | exitFullscreen();
619 | document.body.removeChild(pwPlayer_divNode);
620 | return;
621 | }
622 | }
623 | document.body.removeChild(pwPlayer_divNode);
624 | };
625 |
626 | dragPiPElement(pwPlayer_video_div);
627 |
628 | }
629 |
630 |
631 | function dragPiPElement(elmnt) {
632 | var pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
633 | if (document.getElementById(elmnt.id + "header")) {
634 | // if present, the header is where you move the DIV from:
635 | document.getElementById(elmnt.id + "header").onmousedown = dragMouseDown;
636 | } else {
637 | // otherwise, move the DIV from anywhere inside the DIV:
638 | elmnt.onmousedown = dragMouseDown;
639 | }
640 |
641 | function dragMouseDown(e) {
642 | e = e || window.event;
643 | e.preventDefault();
644 | // get the mouse cursor position at startup:
645 | pos3 = e.clientX;
646 | pos4 = e.clientY;
647 | document.onmouseup = closeDragElement;
648 | // call a function whenever the cursor moves:
649 | document.onmousemove = elementDrag;
650 | }
651 |
652 | function elementDrag(e) {
653 | e = e || window.event;
654 | e.preventDefault();
655 | // calculate the new cursor position:
656 | pos1 = pos3 - e.clientX;
657 | pos2 = pos4 - e.clientY;
658 | pos3 = e.clientX;
659 | pos4 = e.clientY;
660 | // set the element's new position:
661 | elmnt.style.top = (elmnt.offsetTop - pos2) + "px";
662 | elmnt.style.left = (elmnt.offsetLeft - pos1) + "px";
663 | }
664 |
665 | function closeDragElement() {
666 | // stop moving when mouse button is released:
667 | document.onmouseup = null;
668 | document.onmousemove = null;
669 | }
670 | }
671 |
672 |
673 | function doFullScreen() {
674 | var element = document.documentElement;
675 | // Check which implementation is available
676 | var requestMethod = element.requestFullScreen ||
677 | element.webkitRequestFullScreen ||
678 | element.mozRequestFullScreen ||
679 | element.msRequestFullscreen;
680 |
681 | if( requestMethod ) {
682 | // log("fullscreen method found: " + requestMethod);
683 | requestMethod.apply( element );
684 | }else{
685 | // log("fullscreen method not found");
686 | }
687 | ;
688 |
689 | }
690 |
691 | function exitFullscreen(){
692 | var element = document;
693 | // Check which implementation is available
694 | var requestMethod = element.exitFullScreen ||
695 | element.webkitExitFullscreen ||
696 | element.mozCancelFullScreen ||
697 | element.msExitFullscreen;
698 |
699 | if( requestMethod ) {
700 | log ("have method to exit full screen." + requestMethod.toString());
701 | requestMethod.apply( element );
702 | }else{
703 | log ("no method to exit full screen.");
704 | }
705 | }
706 |
707 | function inFullScreen(){
708 | var doc = window.document;
709 | return !(!doc.fullscreenElement
710 | && !doc.mozFullScreenElement
711 | && !doc.webkitFullscreenElement
712 | && !doc.msFullscreenElement);
713 | }
--------------------------------------------------------------------------------