diff --git a/packages/live-ui/index.ts b/packages/live-ui/index.ts new file mode 100644 index 0000000..8f12ad5 --- /dev/null +++ b/packages/live-ui/index.ts @@ -0,0 +1,150 @@ +import { install } from "../../shared/install" +import { type Player } from "@flowplayer/player" +import {BEFORE_PLAY, CLICK, CONFIG, DBL_CLICK, SOURCE, TOUCH_END} from "@flowplayer/player/core/events" + +type SingletonTimer = + | NodeJS.Timeout + | false + +const LIVE_UI_STATE = "is-live-ui" + +export default class LiveUiMiddle extends HTMLElement{ + + constructor(player: Player) { + super() + player.root.classList.toggle(LIVE_UI_STATE, !player.opt("autoplay")) + + this.classList.add("fp-middle") + this.append(...player.createComponents( + flowplayer.defaultElements.MIDDLE_LEFT_ZONE, + flowplayer.defaultElements.MIDDLE_ZONE, + flowplayer.defaultElements.MIDDLE_RIGHT_ZONE + )) + + player.reaper && player.reaper.set("middle", this) + + player.on(BEFORE_PLAY, ()=> { + player.root.classList.remove(LIVE_UI_STATE) + }) + + player.on(CONFIG, ()=> { + if (player.opt("autoplay")) player.root.classList.remove(LIVE_UI_STATE) + }) + + player.on(SOURCE, ()=> { + if (!player.currentSrc) return + if (!player.opts.autoplay) player.root.classList.add(LIVE_UI_STATE) + }) + + this.handleClick(player) + this.handleTouch(player) + this.liveUi(player) + } + + handleClick(player: Player) { + [CLICK, DBL_CLICK].forEach(event => this.addEventListener(event, function (e) { + if (e.defaultPrevented + || player.root.classList.contains("is-endscreen") + || [LIVE_UI_STATE, "is-live"].every(className => player.root.classList.contains(className))) return + + if (event === DBL_CLICK && player.root.classList.contains("no-fullscreen")) return + e.preventDefault() + player.emit(e.type) + return false + })) + + player.on(CLICK, function (e) { + setTimeout(function () { + if (e.defaultPrevented) return // noop + player.togglePlay() + }, 0) + }) + + player.on(DBL_CLICK, function (e) { + setTimeout(function () { + if (e.defaultPrevented) return + player.toggleFullScreen() + }, 0) + }) + } + + handleTouch(player: Player) { + let timer: SingletonTimer = false + + this.addEventListener(TOUCH_END, function (e) { + // touchend is not cancellable during scroll events + if (!e.cancelable || player.hasState("is-endscreen")) return + // do not propogate click events + // so we can do mobile specific behaviors + e.preventDefault() + player.emit(e.type, {source: e}) + }) + player.on(TOUCH_END, function (e) { + // https://github.com/flowplayer/flowplayer-native/issues/301 + setTimeout(function () { + if (e.defaultPrevented) return + // one-touch replay + // https://github.com/flowplayer/flowplayer-native/issues/142 + if (player.ended) return player.togglePlay(true) + + const first_touch = player.hasState("is-starting") + // reset timer + if (timer) clearTimeout(timer) + // first touch should automatically + // start playing the video & if a previous + // touch is valid we should toggle the playing state + if (first_touch || player.hasState("is-touched")) { + if (player.hasState("is-touched")) { + player.setState("is-touched", false) + player.setState("is-touched", false) + } + player.togglePlay() + if (first_touch) return + } + // handle first touch on a video + // that has already started + player.setState("is-touched", true) + // touch menu should be shown on paused menu + if (player.paused) return + // make a 2 second window where another + // touch can toggle the state + timer = setTimeout(function () { + timer = false + player.setState("is-touched", false) + // https://github.com/flowplayer/flowplayer-native/issues/318 + player.setState("is-touched", false) + }, 2000) + }, 0) + }) + } + + liveUi(player: Player) { + const live_ui = document.createElement("div") + live_ui.classList.add("fp-live-ui") + this.append(live_ui) + + const start = document.createElement("button") + start.classList.add("fp-live-start") + start.textContent = player.i18n("core.watch_beginning", "Watch from beginning") + this.setAttribute("aria-label", player.i18n("core.watch_live_start", "Watch from beginning")) + + const live = document.createElement("button") + live.classList.add("fp-live-edge") + live.textContent = player.i18n("core.watch_live", "Watch live") + this.setAttribute("aria-label", player.i18n("core.watch_live", "Watch live")) + + ;[start, live].forEach((button)=> { + button.onclick = (e) => { + player.root.classList.remove(LIVE_UI_STATE) + e.preventDefault() + player.setOpts({start_time: button === start ? 0 : -1}) + + setTimeout(()=> player.togglePlay(true), 200) + } + }) + + live_ui.append(start, live) + } +} + +install("flowplayer-middle", "live-ui-middle", LiveUiMiddle) diff --git a/packages/live-ui/live.css b/packages/live-ui/live.css new file mode 100644 index 0000000..cbfd3a9 --- /dev/null +++ b/packages/live-ui/live.css @@ -0,0 +1,25 @@ +.is-live.is-live-ui .fp-left-zone, .is-live.is-live-ui .fp-right-zone, .is-live.is-live-ui .fp-middle-zone { + display: none; } + +.is-live.is-live-ui .fp-live-ui { + display: flex; } + +.is-live.is-live-ui .fp-middle { + justify-content: center; + align-items: center; } + +.fp-live-ui { + display: none; + width: unset; } + +.fp-live-start { + padding: 0.7em; + margin-right: 0.7em; + border-style: solid; + cursor: pointer; + filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.4)); } + +.fp-live-edge { + padding: 0.7em; + background: #006680; + cursor: pointer; } diff --git a/packages/live-ui/package.json b/packages/live-ui/package.json new file mode 100644 index 0000000..7897d21 --- /dev/null +++ b/packages/live-ui/package.json @@ -0,0 +1,10 @@ +{ + "name": "@flowplayer/components-live-ui", + "main": "./index.ts", + "description": "A screen rendered for live streams before playback starts, with two buttons: one for starting a stream from the beginning and another one for going going live", + "flowplayer": { + "componentName": "live-ui", + "overridenComponent": "flowplayer-middle", + "className": "LiveUiMiddle" + } +}