Implement stream rotation, overhaul local data, add preload script

This moves a bunch of the architectural improvements from the 'iframe'
branch in to main in the pursuit of implementing stream rotation driven
by a stream data field.
This commit is contained in:
Max Goodhart
2020-11-08 13:07:48 -08:00
parent f591685a36
commit c219656564
9 changed files with 492 additions and 272 deletions

View File

@@ -3,69 +3,16 @@ import { Machine, assign } from 'xstate'
import { ensureValidURL } from '../util'
const VIDEO_OVERRIDE_STYLE = `
* {
pointer-events: none;
display: none !important;
position: static !important;
z-index: 0 !important;
}
html, body, video, audio {
display: block !important;
background: black !important;
}
html, body {
overflow: hidden !important;
background: black !important;
}
video, iframe.__video__, audio {
display: block !important;
position: fixed !important;
left: 0 !important;
right: 0 !important;
top: 0 !important;
bottom: 0 !important;
width: 100vw !important;
height: 100vh !important;
object-fit: cover !important;
transition: none !important;
z-index: 999999 !important;
}
audio {
z-index: 999998 !important;
}
.__video_parent__ {
display: block !important;
}
video.__rot180__ {
transform: rotate(180deg) !important;
}
/* For 90 degree rotations, we position the video with swapped width and height and rotate it into place.
It's helpful to offset the video so the transformation is centered in the viewport center.
We move the video top left corner to center of the page and then translate half the video dimensions up and left.
Note that the width and height are swapped in the translate because the video starts with the side dimensions swapped. */
video.__rot90__ {
transform: translate(-50vh, -50vw) rotate(90deg) !important;
}
video.__rot270__ {
transform: translate(-50vh, -50vw) rotate(270deg) !important;
}
video.__rot90__, video.__rot270__ {
left: 50vw !important;
top: 50vh !important;
width: 100vh !important;
height: 100vw !important;
}
`
const viewStateMachine = Machine(
{
id: 'view',
initial: 'empty',
context: {
id: null,
view: null,
pos: null,
content: null,
options: null,
info: {},
},
on: {
@@ -87,39 +34,50 @@ const viewStateMachine = Machine(
}),
cond: 'contentUnchanged',
},
OPTIONS: {
actions: [
assign({
options: (context, event) => event.options,
}),
'sendViewOptions',
],
cond: 'optionsChanged',
},
RELOAD: '.loading',
DEVTOOLS: {
actions: 'openDevTools',
},
VIEW_ERROR: '.error',
VIEW_INFO: {
actions: assign({
info: (context, event) => event.info,
}),
},
},
states: {
loading: {
initial: 'page',
initial: 'navigate',
entry: 'offscreenView',
states: {
page: {
navigate: {
invoke: {
src: 'loadPage',
onDone: {
target: 'video',
target: 'waitForInit',
},
onError: {
target: '#view.displaying.error',
},
},
},
video: {
invoke: {
src: 'startVideo',
onDone: {
target: '#view.displaying.running',
actions: assign({
info: (context, event) => event.data,
}),
},
onError: {
target: '#view.displaying.error',
},
waitForInit: {
on: {
VIEW_INIT: 'waitForVideo',
},
},
waitForVideo: {
on: {
VIEW_LOADED: '#view.displaying.running',
},
},
},
@@ -200,11 +158,18 @@ const viewStateMachine = Machine(
view.webContents.setDevToolsWebContents(inWebContents)
view.webContents.openDevTools({ mode: 'detach' })
},
sendViewOptions: (context, event) => {
const { view } = context
view.webContents.send('options', event.options)
},
},
guards: {
contentUnchanged: (context, event) => {
return isEqual(context.content, event.content)
},
optionsChanged: (context, event) => {
return !isEqual(context.options, event.options)
},
},
services: {
loadPage: async (context, event) => {
@@ -212,149 +177,7 @@ const viewStateMachine = Machine(
ensureValidURL(content.url)
const wc = view.webContents
wc.audioMuted = true
await wc.loadURL(content.url)
if (content.kind === 'video' || content.kind === 'audio') {
wc.insertCSS(VIDEO_OVERRIDE_STYLE, { cssOrigin: 'user' })
} else if (content.kind === 'web') {
wc.insertCSS(
`
html, body {
overflow: hidden !important;
}
`,
{ cssOrigin: 'user' },
)
}
},
startVideo: async (context, event) => {
const { content, view } = context
if (content.kind !== 'video' && content.kind !== 'audio') {
return
}
const wc = view.webContents
// TODO: generalize "video" terminology to "media"
const info = await wc.executeJavaScript(`
(function() {
const sleep = ms => new Promise((resolve) => setTimeout(resolve, ms))
const kind = ${JSON.stringify(
content.kind === 'video' ? 'video' : 'audio',
)}
async function waitForVideo() {
let tries = 0
let video
while ((!video || !video.src) && tries < 10) {
video = document.querySelector(kind)
tries++
await sleep(200)
}
if (video) {
return {video}
}
tries = 0
let iframe
while ((!video || !video.src) && tries < 10) {
for (iframe of document.querySelectorAll('iframe')) {
if (!iframe.contentDocument) {
continue
}
video = iframe.contentDocument.querySelector(kind)
if (video) {
break
}
}
tries++
await sleep(200)
}
if (video) {
return { video, iframe }
}
return {}
}
const periscopeHacks = {
isMatch() {
return location.host === 'www.pscp.tv' || location.host === 'www.periscope.tv'
},
onLoad() {
const playButton = document.querySelector('.PlayButton')
if (playButton) {
playButton.click()
}
},
afterPlay(video) {
const baseVideoEl = document.querySelector('div.BaseVideo')
if (!baseVideoEl) {
return
}
function positionPeriscopeVideo() {
// Periscope videos can be rotated using transform matrix. They need to be rotated correctly.
const tr = baseVideoEl.style.transform
if (tr.endsWith('matrix(0, 1, -1, 0, 0, 0)')) {
video.className = '__rot90__'
} else if (tr.endsWith('matrix(-1, 0, 0, -1, 0)')) {
video.className = '__rot180__'
} else if (tr.endsWith('matrix(0, -1, 1, 0, 0, 0)')) {
video.className = '__rot270__'
}
}
positionPeriscopeVideo()
const obs = new MutationObserver(ml => {
for (const m of ml) {
if (m.attributeName === 'style') {
positionPeriscopeVideo()
return
}
}
})
obs.observe(baseVideoEl, {attributes: true})
},
}
async function findVideo() {
if (periscopeHacks.isMatch()) {
periscopeHacks.onLoad()
}
const { video, iframe } = await waitForVideo()
if (!video) {
throw new Error('could not find video')
}
if (iframe) {
const style = iframe.contentDocument.createElement('style')
style.innerHTML = \`${VIDEO_OVERRIDE_STYLE}\`
iframe.contentDocument.head.appendChild(style)
iframe.className = '__video__'
let parentEl = iframe.parentElement
while (parentEl) {
parentEl.className = '__video_parent__'
parentEl = parentEl.parentElement
}
iframe.contentDocument.body.appendChild(video)
} else {
document.body.appendChild(video)
}
video.muted = false
video.autoPlay = true
video.play()
// Prevent sites from re-muting the video (Periscope, I'm looking at you!)
Object.defineProperty(video, 'muted', {writable: true, value: false})
if (periscopeHacks.isMatch()) {
periscopeHacks.afterPlay(video)
}
const info = { title: document.title }
return info
}
findVideo()
}())
`)
return info
wc.loadURL(content.url)
},
},
},