84 changed files with 67820 additions and 252 deletions
@ -0,0 +1 @@
@@ -0,0 +1 @@
|
||||
webroot/js/web_modules/* linguist-vendored |
@ -1,3 +0,0 @@
@@ -1,3 +0,0 @@
|
||||
[submodule "doc"] |
||||
path = doc |
||||
url = https://github.com/owncast/owncast.github.io/ |
@ -0,0 +1,10 @@
@@ -0,0 +1,10 @@
|
||||
## Third party web dependencies |
||||
|
||||
Owncast's web frontend utilizes a few third party Javascript and CSS dependencies that we ship with the application. |
||||
|
||||
To add, remove, or update one of these components: |
||||
|
||||
1. Perform your `npm install/uninstall/etc`, or edit the `package.json` file to reflect the change you want to make. |
||||
2. Edit the `snowpack` `install` block of `package.json` to specify what files you want to add to the Owncast project. This can be an entire library (such as `preact`) or a single file (such as `video.js/dist/video.min.js`). These paths point to files that live in `node_modules`. |
||||
3. Run `npm run build`. This will download the requested module from NPM, package up the assets you specified, and then copy them to the Owncast web app in the `webroot/js/web_modules` directory. |
||||
4. Your new web dependency is now available for use in your web code. |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,40 @@
@@ -0,0 +1,40 @@
|
||||
{ |
||||
"name": "owncast-dependencies", |
||||
"version": "1.0.0", |
||||
"description": "", |
||||
"main": "index.js", |
||||
"dependencies": { |
||||
"@joeattardi/emoji-button": "^4.2.0", |
||||
"@justinribeiro/lite-youtube": "^0.9.0", |
||||
"@videojs/http-streaming": "^2.2.0", |
||||
"@videojs/themes": "^1.0.0", |
||||
"htm": "^3.0.4", |
||||
"preact": "^10.5.3", |
||||
"showdown": "^1.9.1", |
||||
"tailwindcss": "^1.8.10", |
||||
"video.js": "^7.9.6" |
||||
}, |
||||
"devDependencies": { |
||||
"snowpack": "^2.12.1" |
||||
}, |
||||
"snowpack": { |
||||
"install": [ |
||||
"video.js/dist/video.min.js", |
||||
"@videojs/themes/fantasy/*", |
||||
"@videojs/http-streaming/dist/videojs-http-streaming.min.js", |
||||
"video.js/dist/video-js.min.css", |
||||
"@joeattardi/emoji-button", |
||||
"@justinribeiro/lite-youtube", |
||||
"htm", |
||||
"preact", |
||||
"showdown", |
||||
"tailwindcss/dist/tailwind.min.css" |
||||
] |
||||
}, |
||||
"scripts": { |
||||
"test": "echo \"Error: no test specified\" && exit 1", |
||||
"build": "npm install && npx snowpack install && cp -R web_modules ../../webroot/js" |
||||
}, |
||||
"author": "", |
||||
"license": "ISC" |
||||
} |
@ -0,0 +1,12 @@
@@ -0,0 +1,12 @@
|
||||
package config |
||||
|
||||
import "path/filepath" |
||||
|
||||
const ( |
||||
WebRoot = "webroot" |
||||
PrivateHLSStoragePath = "hls" |
||||
) |
||||
|
||||
var ( |
||||
PublicHLSStoragePath = filepath.Join(WebRoot, "hls") |
||||
) |
@ -0,0 +1,16 @@
@@ -0,0 +1,16 @@
|
||||
package admin |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"net/http" |
||||
|
||||
"github.com/owncast/owncast/metrics" |
||||
) |
||||
|
||||
// GetHardwareStats will return hardware utilization over time
|
||||
func GetHardwareStats(w http.ResponseWriter, r *http.Request) { |
||||
metrics := metrics.Metrics |
||||
|
||||
w.Header().Set("Content-Type", "application/json") |
||||
json.NewEncoder(w).Encode(metrics) |
||||
} |
@ -0,0 +1,513 @@
@@ -0,0 +1,513 @@
|
||||
openapi: 3.0.1 |
||||
info: |
||||
title: Owncast |
||||
description: Owncast is a self-hosted live video and web chat server for use with existing popular broadcasting software. |
||||
version: '0.0.2' |
||||
servers: [] |
||||
|
||||
tags: |
||||
- name: Admin |
||||
description: Admin operations requiring authentication. |
||||
- name: Chat |
||||
description: Endpoints related to the chat interface. |
||||
|
||||
components: |
||||
schemas: |
||||
BasicResponse: |
||||
type: object |
||||
properties: |
||||
success: |
||||
type: boolean |
||||
message: |
||||
type: string |
||||
InstanceDetails: |
||||
type: object |
||||
properties: |
||||
name: |
||||
type: string |
||||
title: |
||||
type: string |
||||
summary: |
||||
type: string |
||||
description: This is brief summary of whom you are or what the stream is. |
||||
logo: |
||||
type: object |
||||
properties: |
||||
large: |
||||
type: string |
||||
small: |
||||
type: string |
||||
tags: |
||||
type: array |
||||
items: |
||||
type: string |
||||
socialHandles: |
||||
type: array |
||||
items: |
||||
type: object |
||||
properties: |
||||
platform: |
||||
type: string |
||||
example: github |
||||
url: |
||||
type: string |
||||
example: http://github.com/owncast/owncast |
||||
extraUserInfoFileName: |
||||
type: string |
||||
description: Path to additional content about the server. |
||||
version: |
||||
type: string |
||||
example: Owncast v0.0.2-macOS (ef3796a033b32a312ebf5b334851cbf9959e7ecb) |
||||
S3: |
||||
type: object |
||||
properties: |
||||
enabled: |
||||
type: boolean |
||||
endpoint: |
||||
type: string |
||||
servingEndpoint: |
||||
type: string |
||||
accessKey: |
||||
type: string |
||||
secret: |
||||
type: string |
||||
bucket: |
||||
type: string |
||||
region: |
||||
type: string |
||||
acl: |
||||
type: string |
||||
required: |
||||
- enabled |
||||
StreamQuality: |
||||
type: object |
||||
properties: |
||||
videoPassthrough: |
||||
type: boolean |
||||
audioPassthrough: |
||||
type: boolean |
||||
videoBitrate: |
||||
type: integer |
||||
audioBitrate: |
||||
type: integer |
||||
scaledWidth: |
||||
type: integer |
||||
scaledHeight: |
||||
type: integer |
||||
framerate: |
||||
type: integer |
||||
encoderPreset: |
||||
type: string |
||||
TimestampedValue: |
||||
type: object |
||||
properties: |
||||
time: |
||||
type: string |
||||
format: date-time |
||||
value: |
||||
type: integer |
||||
|
||||
|
||||
securitySchemes: |
||||
AdminBasicAuth: |
||||
type: http |
||||
scheme: basic |
||||
description: The username for admin basic auth is `admin` and the password is the stream key. |
||||
|
||||
responses: |
||||
BasicResponse: |
||||
description: Operation Success/Failure Response |
||||
content: |
||||
application/json: |
||||
schema: |
||||
$ref: "#/components/schemas/BasicResponse" |
||||
examples: |
||||
success: |
||||
summary: Operation succeeded. |
||||
value: {"success": true, "message": "inbound stream disconnected"} |
||||
failure: |
||||
summary: Operation failed. |
||||
value: {"success": false, "message": "no inbound stream connected"} |
||||
|
||||
paths: |
||||
|
||||
/api/config: |
||||
get: |
||||
summary: Information |
||||
description: Get the public information about the server. Adds context to the server, as well as information useful for the user interface. |
||||
tags: ["Server"] |
||||
responses: |
||||
'200': |
||||
description: "" |
||||
content: |
||||
application/json: |
||||
schema: |
||||
$ref: "#/components/schemas/InstanceDetails" |
||||
|
||||
/api/status: |
||||
get: |
||||
summary: Current Status |
||||
description: This endpoint is used to discover when a server is broadcasting, the number of active viewers as well as other useful information for updating the user interface. |
||||
tags: ["Server"] |
||||
responses: |
||||
'200': |
||||
description: "" |
||||
content: |
||||
application/json: |
||||
schema: |
||||
type: object |
||||
properties: |
||||
lastConnectTime: |
||||
type: string |
||||
nullable: true |
||||
format: date-time |
||||
overallMaxViewerCount: |
||||
type: integer |
||||
sessionMaxViewerCount: |
||||
type: integer |
||||
online: |
||||
type: boolean |
||||
viewerCount: |
||||
type: integer |
||||
lastDisconnectTime: |
||||
type: string |
||||
nullable: true |
||||
format: date-time |
||||
examples: |
||||
online: |
||||
value: |
||||
lastConnectTime: "2020-10-03T21:36:22-05:00" |
||||
lastDisconnectTime: null |
||||
online: true |
||||
overallMaxViewerCount: 420 |
||||
sessionMaxViewerCount: 12 |
||||
viewerCount: 7 |
||||
|
||||
/api/chat: |
||||
get: |
||||
summary: Historical Chat Messages |
||||
description: Used to get all chat messages prior to connecting to the websocket. |
||||
tags: ["Chat"] |
||||
responses: |
||||
'200': |
||||
description: "" |
||||
content: |
||||
application/json: |
||||
schema: |
||||
type: array |
||||
items: |
||||
type: object |
||||
properties: |
||||
author: |
||||
type: string |
||||
description: Username of the chat message poster. |
||||
body: |
||||
type: string |
||||
description: Escaped HTML of the chat message content. |
||||
image: |
||||
type: string |
||||
description: URL of the chat user avatar. |
||||
id: |
||||
type: string |
||||
description: Unique ID of the chat message. |
||||
visible: |
||||
type: boolean |
||||
description: "TODO" |
||||
timestamp: |
||||
type: string |
||||
format: date-time |
||||
|
||||
/api/yp: |
||||
get: |
||||
summary: Yellow Pages Information |
||||
description: Information to be used in the Yellow Pages service, a global directory of Owncast servers. |
||||
tags: ["Server"] |
||||
responses: |
||||
'200': |
||||
description: "" |
||||
content: |
||||
application/json: |
||||
schema: |
||||
type: object |
||||
properties: |
||||
name: |
||||
type: string |
||||
description: |
||||
type: string |
||||
logo: |
||||
type: string |
||||
nsfw: |
||||
type: boolean |
||||
tags: |
||||
type: array |
||||
items: |
||||
type: string |
||||
online: |
||||
type: boolean |
||||
viewerCount: |
||||
type: integer |
||||
overallMaxViewerCount: |
||||
type: integer |
||||
sessionMaxViewerCount: |
||||
type: integer |
||||
lastConnectTime: |
||||
type: string |
||||
nullable: true |
||||
format: date-time |
||||
|
||||
/api/emoji: |
||||
get: |
||||
summary: Get Custom Emoji |
||||
description: Get a list of custom emoji that are supported in chat. |
||||
tags: ["Chat"] |
||||
responses: |
||||
'200': |
||||
description: "" |
||||
content: |
||||
application/json: |
||||
schema: |
||||
type: array |
||||
items: |
||||
type: object |
||||
properties: |
||||
name: |
||||
type: string |
||||
description: The name of the Emoji |
||||
emoji: |
||||
type: string |
||||
description: The relative path to the Emoji image file |
||||
examples: |
||||
default: |
||||
value: |
||||
items: |
||||
- name: nicolas_cage_party |
||||
emoji: /img/emoji/nicolas_cage_party.gif |
||||
- name: parrot |
||||
emoji: /img/emoji/parrot.gif |
||||
|
||||
/api/admin/broadcaster: |
||||
get: |
||||
summary: "Broadcaster Details" |
||||
tags: ["Admin"] |
||||
security: |
||||
- AdminBasicAuth: [] |
||||
responses: |
||||
'200': |
||||
description: Connected Broadcaster Details |
||||
content: |
||||
application/json: |
||||
schema: |
||||
type: object |
||||
properties: |
||||
success: |
||||
type: boolean |
||||
message: |
||||
type: string |
||||
broadcaster: |
||||
type: object |
||||
properties: |
||||
remoteAddr: |
||||
type: string |
||||
time: |
||||
type: string |
||||
format: date-time |
||||
streamDetails: |
||||
type: object |
||||
properties: |
||||
width: |
||||
type: integer |
||||
height: |
||||
type: integer |
||||
frameRate: |
||||
type: integer |
||||
videoBitrate: |
||||
type: integer |
||||
videoCodec: |
||||
type: string |
||||
audioBitrate: |
||||
type: integer |
||||
audioCodec: |
||||
type: string |
||||
encoder: |
||||
type: string |
||||
examples: |
||||
connected: |
||||
summary: "Broadcaster Connected" |
||||
value: |
||||
success: true |
||||
message: "" |
||||
broadcaster: |
||||
remoteAddr: 127.0.0.1 |
||||
time: "TODO" |
||||
streamDetails: |
||||
width: 640 |
||||
height: 480 |
||||
frameRate: 24 |
||||
videoBitrate: 1500 |
||||
videoCodec: "todo" |
||||
audioBitrate: 256 |
||||
audioCodec: "aac" |
||||
encoder: "todo" |
||||
not-connected: |
||||
summary: "Broadcaster Not Connected" |
||||
value: |
||||
success: false |
||||
message: "no broadcaster connected" |
||||
|
||||
/api/admin/disconnect: |
||||
post: |
||||
summary: Disconnect Broadcaster |
||||
description: Disconnect the active inbound stream, if one exists, and terminate the broadcast. |
||||
tags: ["Admin"] |
||||
security: |
||||
- AdminBasicAuth: [] |
||||
responses: |
||||
'200': |
||||
$ref: "#/components/responses/BasicResponse" |
||||
|
||||
|
||||
/api/admin/changekey: |
||||
post: |
||||
summary: Update Stream Key |
||||
description: Change the stream key in memory, but not in the config file. This will require all broadcasters to be reconfigured to connect again. |
||||
tags: ["Admin"] |
||||
security: |
||||
- AdminBasicAuth: [] |
||||
requestBody: |
||||
description: "" |
||||
required: true |
||||
content: |
||||
application/json: |
||||
schema: |
||||
type: object |
||||
properties: |
||||
key: |
||||
type: string |
||||
responses: |
||||
'200': |
||||
description: Stream was disconnected. |
||||
content: |
||||
application/json: |
||||
schema: |
||||
type: object |
||||
properties: |
||||
success: |
||||
type: boolean |
||||
example: true |
||||
message: |
||||
type: string |
||||
example: changed |
||||
|
||||
/api/admin/serverconfig: |
||||
get: |
||||
summary: Server Configuration |
||||
description: Get the current configuration of the Owncast server. |
||||
tags: ["Admin"] |
||||
security: |
||||
- AdminBasicAuth: [] |
||||
responses: |
||||
'200': |
||||
description: "" |
||||
content: |
||||
application/json: |
||||
schema: |
||||
type: object |
||||
properties: |
||||
instanceDetails: |
||||
$ref: "#/components/schemas/InstanceDetails" |
||||
ffmpegPath: |
||||
type: string |
||||
webServerPort: |
||||
type: integer |
||||
s3: |
||||
$ref: "#/components/schemas/S3" |
||||
videoSettings: |
||||
type: object |
||||
properties: |
||||
videoQualityVariants: |
||||
type: array |
||||
items: |
||||
$ref: "#/components/schemas/StreamQuality" |
||||
segmentLengthSeconds: |
||||
type: integer |
||||
numberOfPlaylistItems: |
||||
type: integer |
||||
|
||||
/api/admin/viewersOverTime: |
||||
get: |
||||
summary: Viewers Over Time |
||||
description: Get the tracked viewer count over the collected period. |
||||
tags: ["Admin"] |
||||
security: |
||||
- AdminBasicAuth: [] |
||||
responses: |
||||
'200': |
||||
description: "" |
||||
content: |
||||
application/json: |
||||
schema: |
||||
type: array |
||||
items: |
||||
$ref: "#/components/schemas/TimestampedValue" |
||||
examples: |
||||
default: |
||||
value: |
||||
- time: "2020-10-03T21:41:00.381996-05:00" |
||||
value: 50 |
||||
- time: "2020-10-03T21:42:00.381996-05:00" |
||||
value: 52 |
||||
|
||||
|
||||
|
||||
|
||||
/api/admin/hardwarestats: |
||||
get: |
||||
summary: Hardware Stats |
||||
description: "Get the CPU, Memory and Disk utilization levels over the collected period." |
||||
tags: ["Admin"] |
||||
security: |
||||
- AdminBasicAuth: [] |
||||
responses: |
||||
'200': |
||||
description: "" |
||||
content: |
||||
application/json: |
||||
schema: |
||||
type: object |
||||
properties: |
||||
cpu: |
||||
type: array |
||||
items: |
||||
$ref: "#/components/schemas/TimestampedValue" |
||||
memory: |
||||
type: array |
||||
items: |
||||
$ref: "#/components/schemas/TimestampedValue" |
||||
disk: |
||||
type: array |
||||
items: |
||||
$ref: "#/components/schemas/TimestampedValue" |
||||
examples: |
||||
default: |
||||
value: |
||||
cpu: |
||||
- time: "2020-10-03T21:41:00.381996-05:00" |
||||
value: 23 |
||||
- time: "2020-10-03T21:42:00.381996-05:00" |
||||
value: 27 |
||||
- time: "2020-10-03T21:43:00.381996-05:00" |
||||
value: 22 |
||||
memory: |
||||
- time: "2020-10-03T21:41:00.381996-05:00" |
||||
value: 65 |
||||
- time: "2020-10-03T21:42:00.381996-05:00" |
||||
value: 66 |
||||
- time: "2020-10-03T21:43:00.381996-05:00" |
||||
value: 72 |
||||
disk: |
||||
- time: "2020-10-03T21:41:00.381996-05:00" |
||||
value: 11 |
||||
- time: "2020-10-03T21:42:00.381996-05:00" |
||||
value: 11 |
||||
- time: "2020-10-03T21:43:00.381996-05:00" |
||||
value: 11 |
File diff suppressed because one or more lines are too long
@ -0,0 +1,301 @@
@@ -0,0 +1,301 @@
|
||||
/** |
||||
* |
||||
* The shadowDom / Intersection Observer version of Paul's concept: |
||||
* https://github.com/paulirish/lite-youtube-embed
|
||||
* |
||||
* A lightweight YouTube embed. Still should feel the same to the user, just |
||||
* MUCH faster to initialize and paint. |
||||
* |
||||
* Thx to these as the inspiration |
||||
* https://storage.googleapis.com/amp-vs-non-amp/youtube-lazy.html
|
||||
* https://autoplay-youtube-player.glitch.me/
|
||||
* |
||||
* Once built it, I also found these (👍👍): |
||||
* https://github.com/ampproject/amphtml/blob/master/extensions/amp-youtube
|
||||
* https://github.com/Daugilas/lazyYT https://github.com/vb/lazyframe
|
||||
*/ |
||||
class LiteYTEmbed extends HTMLElement { |
||||
constructor() { |
||||
super(); |
||||
this.iframeLoaded = false; |
||||
this.setupDom(); |
||||
} |
||||
static get observedAttributes() { |
||||
return ['videoid']; |
||||
} |
||||
connectedCallback() { |
||||
this.addEventListener('pointerover', LiteYTEmbed.warmConnections, { |
||||
once: true, |
||||
}); |
||||
this.addEventListener('click', () => this.addIframe()); |
||||
} |
||||
get videoId() { |
||||
return encodeURIComponent(this.getAttribute('videoid') || ''); |
||||
} |
||||
set videoId(id) { |
||||
this.setAttribute('videoid', id); |
||||
} |
||||
get videoTitle() { |
||||
return this.getAttribute('videotitle') || 'Video'; |
||||
} |
||||
set videoTitle(title) { |
||||
this.setAttribute('videotitle', title); |
||||
} |
||||
get videoPlay() { |
||||
return this.getAttribute('videoPlay') || 'Play'; |
||||
} |
||||
set videoPlay(name) { |
||||
this.setAttribute('videoPlay', name); |
||||
} |
||||
get videoStartAt() { |
||||
return Number(this.getAttribute('videoStartAt') || '0'); |
||||
} |
||||
set videoStartAt(time) { |
||||
this.setAttribute('videoStartAt', String(time)); |
||||
} |
||||
get autoLoad() { |
||||
return this.hasAttribute('autoload'); |
||||
} |
||||
set autoLoad(value) { |
||||
if (value) { |
||||
this.setAttribute('autoload', ''); |
||||
} |
||||
else { |
||||
this.removeAttribute('autoload'); |
||||
} |
||||
} |
||||
get params() { |
||||
return `start=${this.videoStartAt}&${this.getAttribute('params')}`; |
||||
} |
||||
/** |
||||
* Define our shadowDOM for the component |
||||
*/ |
||||
setupDom() { |
||||
const shadowDom = this.attachShadow({ mode: 'open' }); |
||||
shadowDom.innerHTML = ` |
||||
<style> |
||||
:host { |
||||
contain: content; |
||||
display: block; |
||||
position: relative; |
||||
width: 100%; |
||||
padding-bottom: calc(100% / (16 / 9)); |
||||
} |
||||
|
||||
#frame, #fallbackPlaceholder, iframe { |
||||
position: absolute; |
||||
width: 100%; |
||||
height: 100%; |
||||
} |
||||
|
||||
#frame { |
||||
cursor: pointer; |
||||
} |
||||
|
||||
#fallbackPlaceholder { |
||||
object-fit: cover; |
||||
} |
||||
|
||||
#frame::before { |
||||
content: ''; |
||||
display: block; |
||||
position: absolute; |
||||
top: 0; |
||||
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAADGCAYAAAAT+OqFAAAAdklEQVQoz42QQQ7AIAgEF/T/D+kbq/RWAlnQyyazA4aoAB4FsBSA/bFjuF1EOL7VbrIrBuusmrt4ZZORfb6ehbWdnRHEIiITaEUKa5EJqUakRSaEYBJSCY2dEstQY7AuxahwXFrvZmWl2rh4JZ07z9dLtesfNj5q0FU3A5ObbwAAAABJRU5ErkJggg==); |
||||
background-position: top; |
||||
background-repeat: repeat-x; |
||||
height: 60px; |
||||
padding-bottom: 50px; |
||||
width: 100%; |
||||
transition: all 0.2s cubic-bezier(0, 0, 0.2, 1); |
||||
z-index: 1; |
||||
} |
||||
/* play button */ |
||||
.lty-playbtn { |
||||
width: 70px; |
||||
height: 46px; |
||||
background-color: #212121; |
||||
z-index: 1; |
||||
opacity: 0.8; |
||||
border-radius: 14%; /* TODO: Consider replacing this with YT's actual svg. Eh. */ |
||||
transition: all 0.2s cubic-bezier(0, 0, 0.2, 1); |
||||
border: 0; |
||||
} |
||||
#frame:hover .lty-playbtn { |
||||
background-color: #f00; |
||||
opacity: 1; |
||||
} |
||||
/* play button triangle */ |
||||
.lty-playbtn:before { |
||||
content: ''; |
||||
border-style: solid; |
||||
border-width: 11px 0 11px 19px; |
||||
border-color: transparent transparent transparent #fff; |
||||
} |
||||
.lty-playbtn, |
||||
.lty-playbtn:before { |
||||
position: absolute; |
||||
top: 50%; |
||||
left: 50%; |
||||
transform: translate3d(-50%, -50%, 0); |
||||
} |
||||
|
||||
/* Post-click styles */ |
||||
.lyt-activated { |
||||
cursor: unset; |
||||
} |
||||
|
||||
#frame.lyt-activated::before, |
||||
.lyt-activated .lty-playbtn { |
||||
display: none; |
||||
} |
||||
</style> |
||||
<div id="frame"> |
||||
<picture> |
||||
<source id="webpPlaceholder" type="image/webp"> |
||||
<source id="jpegPlaceholder" type="image/jpeg"> |
||||
<img id="fallbackPlaceholder" referrerpolicy="origin"> |
||||
</picture> |
||||
<button class="lty-playbtn"></button> |
||||
</div> |
||||
`;
|
||||
this.domRefFrame = this.shadowRoot.querySelector('#frame'); |
||||
this.domRefImg = { |
||||
fallback: this.shadowRoot.querySelector('#fallbackPlaceholder'), |
||||
webp: this.shadowRoot.querySelector('#webpPlaceholder'), |
||||
jpeg: this.shadowRoot.querySelector('#jpegPlaceholder'), |
||||
}; |
||||
this.domRefPlayButton = this.shadowRoot.querySelector('.lty-playbtn'); |
||||
} |
||||
/** |
||||
* Parse our attributes and fire up some placeholders |
||||
*/ |
||||
setupComponent() { |
||||
this.initImagePlaceholder(); |
||||
this.domRefPlayButton.setAttribute('aria-label', `${this.videoPlay}: ${this.videoTitle}`); |
||||
this.setAttribute('title', `${this.videoPlay}: ${this.videoTitle}`); |
||||
if (this.autoLoad) { |
||||
this.initIntersectionObserver(); |
||||
} |
||||
} |
||||
/** |
||||
* Lifecycle method that we use to listen for attribute changes to period |
||||
* @param {*} name |
||||
* @param {*} oldVal |
||||
* @param {*} newVal |
||||
*/ |
||||
attributeChangedCallback(name, oldVal, newVal) { |
||||
switch (name) { |
||||
case 'videoid': { |
||||
if (oldVal !== newVal) { |
||||
this.setupComponent(); |
||||
// if we have a previous iframe, remove it and the activated class
|
||||
if (this.domRefFrame.classList.contains('lyt-activated')) { |
||||
this.domRefFrame.classList.remove('lyt-activated'); |
||||
this.shadowRoot.querySelector('iframe').remove(); |
||||
} |
||||
} |
||||
break; |
||||
} |
||||
} |
||||
} |
||||
/** |
||||
* Inject the iframe into the component body |
||||
*/ |
||||
addIframe() { |
||||
if (!this.iframeLoaded) { |
||||
const iframeHTML = ` |
||||
<iframe frameborder="0" |
||||
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen |
||||
src="https://www.youtube.com/embed/${this.videoId}?autoplay=1&${this.params}" |
||||
></iframe>`; |
||||
this.domRefFrame.insertAdjacentHTML('beforeend', iframeHTML); |
||||
this.domRefFrame.classList.add('lyt-activated'); |
||||
this.iframeLoaded = true; |
||||
} |
||||
} |
||||
/** |
||||
* Setup the placeholder image for the component |
||||
*/ |
||||
initImagePlaceholder() { |
||||
// we don't know which image type to preload, so warm the connection
|
||||
LiteYTEmbed.addPrefetch('preconnect', 'https://i.ytimg.com/'); |
||||
const posterUrlWebp = `https://i.ytimg.com/vi_webp/${this.videoId}/hqdefault.webp`; |
||||
const posterUrlJpeg = `https://i.ytimg.com/vi/${this.videoId}/hqdefault.jpg`; |
||||
this.domRefImg.webp.srcset = posterUrlWebp; |
||||
this.domRefImg.jpeg.srcset = posterUrlJpeg; |
||||
this.domRefImg.fallback.src = posterUrlJpeg; |
||||
this.domRefImg.fallback.setAttribute('aria-label', `${this.videoPlay}: ${this.videoTitle}`); |
||||
this.domRefImg.fallback.setAttribute('alt', `${this.videoPlay}: ${this.videoTitle}`); |
||||
} |
||||
/** |
||||
* Setup the Intersection Observer to load the iframe when scrolled into view |
||||
*/ |
||||
initIntersectionObserver() { |
||||
if ('IntersectionObserver' in window && |
||||
'IntersectionObserverEntry' in window) { |
||||
const options = { |
||||
root: null, |
||||
rootMargin: '0px', |
||||
threshold: 0, |
||||
}; |
||||
const observer = new IntersectionObserver((entries, observer) => { |
||||
entries.forEach(entry => { |
||||
if (entry.isIntersecting && !this.iframeLoaded) { |
||||
LiteYTEmbed.warmConnections(); |
||||
this.addIframe(); |
||||
observer.unobserve(this); |
||||
} |
||||
}); |
||||
}, options); |
||||
observer.observe(this); |
||||
} |
||||
} |
||||
/** |
||||
* Add a <link rel={preload | preconnect} ...> to the head |
||||
* @param {*} kind |
||||
* @param {*} url |
||||
* @param {*} as |
||||
*/ |
||||
static addPrefetch(kind, url, as) { |
||||
const linkElem = document.createElement('link'); |
||||
linkElem.rel = kind; |
||||
linkElem.href = url; |
||||
if (as) { |
||||
linkElem.as = as; |
||||
} |
||||
linkElem.crossOrigin = 'true'; |
||||
document.head.append(linkElem); |
||||
} |
||||
/** |
||||
* Begin preconnecting to warm up the iframe load Since the embed's netwok |
||||
* requests load within its iframe, preload/prefetch'ing them outside the |
||||
* iframe will only cause double-downloads. So, the best we can do is warm up |
||||
* a few connections to origins that are in the critical path. |
||||
* |
||||
* Maybe `<link rel=preload as=document>` would work, but it's unsupported: |
||||
* http://crbug.com/593267 But TBH, I don't think it'll happen soon with Site
|
||||
* Isolation and split caches adding serious complexity. |
||||
*/ |
||||
static warmConnections() { |
||||
if (LiteYTEmbed.preconnected) |
||||
return; |
||||
// Host that YT uses to serve JS needed by player, per amp-youtube
|
||||
LiteYTEmbed.addPrefetch('preconnect', 'https://s.ytimg.com'); |
||||
// The iframe document and most of its subresources come right off
|
||||
// youtube.com
|
||||
LiteYTEmbed.addPrefetch('preconnect', 'https://www.youtube.com'); |
||||
// The botguard script is fetched off from google.com
|
||||
LiteYTEmbed.addPrefetch('preconnect', 'https://www.google.com'); |
||||
// TODO: Not certain if these ad related domains are in the critical path.
|
||||
// Could verify with domain-specific throttling.
|
||||
LiteYTEmbed.addPrefetch('preconnect', 'https://googleads.g.doubleclick.net'); |
||||
LiteYTEmbed.addPrefetch('preconnect', 'https://static.doubleclick.net'); |
||||
LiteYTEmbed.preconnected = true; |
||||
} |
||||
} |
||||
LiteYTEmbed.preconnected = false; |
||||
// Register custom element
|
||||
customElements.define('lite-youtube', LiteYTEmbed); |
||||
|
||||
export { LiteYTEmbed }; |
File diff suppressed because one or more lines are too long
@ -0,0 +1,116 @@
@@ -0,0 +1,116 @@
|
||||
.vjs-theme-fantasy { |
||||
--vjs-theme-fantasy--primary: #9f44b4; |
||||
--vjs-theme-fantasy--secondary: #fff; |
||||
} |
||||
|
||||
.vjs-theme-fantasy .vjs-big-play-button { |
||||
width: 70px; |
||||
height: 70px; |
||||
background: none; |
||||
line-height: 70px; |
||||
font-size: 80px; |
||||
border: none; |
||||
top: 50%; |
||||
left: 50%; |
||||
margin-top: -35px; |
||||
margin-left: -35px; |
||||
color: var(--vjs-theme-fantasy--primary); |
||||
} |
||||
|
||||
.vjs-theme-fantasy:hover .vjs-big-play-button, |
||||
.vjs-theme-fantasy.vjs-big-play-button:focus { |
||||
background-color: transparent; |
||||
color: #fff; |
||||
} |
||||
|
||||
.vjs-theme-fantasy .vjs-control-bar { |
||||
height: 54px; |
||||
} |
||||
|
||||
.vjs-theme-fantasy .vjs-button > .vjs-icon-placeholder::before { |
||||
line-height: 54px; |
||||
} |
||||
|
||||
.vjs-theme-fantasy .vjs-time-control { |
||||
line-height: 54px; |
||||
} |
||||
|
||||
/* Play Button */ |
||||
.vjs-theme-fantasy .vjs-play-control { |
||||
font-size: 1.5em; |
||||
position: relative; |
||||
} |
||||
|
||||
.vjs-theme-fantasy .vjs-volume-panel { |
||||
order: 4; |
||||
} |
||||
|
||||
.vjs-theme-fantasy .vjs-volume-bar { |
||||
margin-top: 2.5em; |
||||
} |
||||
|
||||
.vjs-theme-city .vjs-volume-panel:hover .vjs-volume-control.vjs-volume-horizontal { |
||||
height: 100%; |
||||
} |
||||
|
||||
.vjs-theme-fantasy .vjs-progress-control .vjs-progress-holder { |
||||
font-size: 1.5em; |
||||
} |
||||
|
||||
.vjs-theme-fantasy .vjs-progress-control:hover .vjs-progress-holder { |
||||
font-size: 1.5em; |
||||
} |
||||
|
||||
.vjs-theme-fantasy .vjs-play-control .vjs-icon-placeholder::before { |
||||
height: 1.3em; |
||||
width: 1.3em; |
||||
margin-top: 0.2em; |
||||
border-radius: 1em; |
||||
border: 3px solid var(--vjs-theme-fantasy--secondary); |
||||
top: 2px; |
||||
left: 9px; |
||||
line-height: 1.1; |
||||
} |
||||
|
||||
.vjs-theme-fantasy .vjs-play-control:hover .vjs-icon-placeholder::before { |
||||
border: 3px solid var(--vjs-theme-fantasy--secondary); |
||||
} |
||||
|
||||
.vjs-theme-fantasy .vjs-play-progress { |
||||
background-color: var(--vjs-theme-fantasy--primary); |
||||
} |
||||
|
||||
.vjs-theme-fantasy .vjs-play-progress::before { |
||||
height: 0.8em; |
||||
width: 0.8em; |
||||
content: ''; |
||||
background-color: var(--vjs-theme-fantasy--primary); |
||||
border: 4px solid var(--vjs-theme-fantasy--secondary); |
||||
border-radius: 0.8em; |
||||
top: -0.25em; |
||||
} |
||||
|
||||
.vjs-theme-fantasy .vjs-progress-control { |
||||
font-size: 14px; |
||||
} |
||||
|
||||
.vjs-theme-fantasy .vjs-fullscreen-control { |
||||
order: 6; |
||||
} |
||||
|
||||
.vjs-theme-fantasy .vjs-remaining-time { |
||||
display: none; |
||||
} |
||||
|
||||
/* Nyan version */ |
||||
.vjs-theme-fantasy.nyan .vjs-play-progress { |
||||
background: linear-gradient(to bottom, #fe0000 0%, #fe9a01 16.666666667%, #fe9a01 16.666666667%, #ffff00 33.332666667%, #ffff00 33.332666667%, #32ff00 49.999326667%, #32ff00 49.999326667%, #0099fe 66.6659926%, #0099fe 66.6659926%, #6633ff 83.33266%, #6633ff 83.33266%); |
||||
} |
||||
|
||||
.vjs-theme-fantasy.nyan .vjs-play-progress::before { |
||||
height: 1.3em; |
||||
width: 1.3em; |
||||
background: svg-load('icons/nyan-cat.svg', fill=#fff) no-repeat; |
||||
border: none; |
||||
top: -0.35em; |
||||
} |
@ -0,0 +1,25 @@
@@ -0,0 +1,25 @@
|
||||
var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; |
||||
|
||||
function getDefaultExportFromCjs (x) { |
||||
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x; |
||||
} |
||||
|
||||
function createCommonjsModule(fn, basedir, module) { |
||||
return module = { |
||||
path: basedir, |
||||
exports: {}, |
||||
require: function (path, base) { |
||||
return commonjsRequire(path, (base === undefined || base === null) ? module.path : base); |
||||
} |
||||
}, fn(module, module.exports), module.exports; |
||||
} |
||||
|
||||
function getDefaultExportFromNamespaceIfNotNamed (n) { |
||||
return n && Object.prototype.hasOwnProperty.call(n, 'default') && Object.keys(n).length === 1 ? n['default'] : n; |
||||
} |
||||
|
||||
function commonjsRequire () { |
||||
throw new Error('Dynamic requires are not currently supported by @rollup/plugin-commonjs'); |
||||
} |
||||
|
||||
export { commonjsGlobal as a, getDefaultExportFromNamespaceIfNotNamed as b, createCommonjsModule as c, getDefaultExportFromCjs as g }; |
@ -0,0 +1,44 @@
@@ -0,0 +1,44 @@
|
||||
import { b as getDefaultExportFromNamespaceIfNotNamed, a as commonjsGlobal } from './_commonjsHelpers-37fa8da4.js'; |
||||
|
||||
var _nodeResolve_empty = {}; |
||||
|
||||
var _nodeResolve_empty$1 = /*#__PURE__*/Object.freeze({ |
||||
__proto__: null, |
||||
'default': _nodeResolve_empty |
||||
}); |
||||
|
||||
var minDoc = /*@__PURE__*/getDefaultExportFromNamespaceIfNotNamed(_nodeResolve_empty$1); |
||||
|
||||
var topLevel = typeof commonjsGlobal !== 'undefined' ? commonjsGlobal : |
||||
typeof window !== 'undefined' ? window : {}; |
||||
|
||||
|
||||
var doccy; |
||||
|
||||
if (typeof document !== 'undefined') { |
||||
doccy = document; |
||||
} else { |
||||
doccy = topLevel['__GLOBAL_DOCUMENT_CACHE@4']; |
||||
|
||||
if (!doccy) { |
||||
doccy = topLevel['__GLOBAL_DOCUMENT_CACHE@4'] = minDoc; |
||||
} |
||||
} |
||||
|
||||
var document_1 = doccy; |
||||
|
||||
var win; |
||||
|
||||
if (typeof window !== "undefined") { |
||||
win = window; |
||||
} else if (typeof commonjsGlobal !== "undefined") { |
||||
win = commonjsGlobal; |
||||
} else if (typeof self !== "undefined"){ |
||||
win = self; |
||||
} else { |
||||
win = {}; |
||||
} |
||||
|
||||
var window_1 = win; |
||||
|
||||
export { document_1 as d, window_1 as w }; |
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
var n=function(t,s,r,e){var u;s[0]=0;for(var h=1;h<s.length;h++){var p=s[h++],a=s[h]?(s[0]|=p?1:2,r[s[h++]]):s[++h];3===p?e[0]=a:4===p?e[1]=Object.assign(e[1]||{},a):5===p?(e[1]=e[1]||{})[s[++h]]=a:6===p?e[1][s[++h]]+=a+"":p?(u=t.apply(a,n(t,a,r,["",null])),e.push(u),a[0]?s[0]|=2:(s[h-2]=0,s[h]=u)):e.push(a);}return e},t=new Map;function htm_module(s){var r=t.get(this);return r||(r=new Map,t.set(this,r)),(r=n(this,r.get(s)||(r.set(s,r=function(n){for(var t,s,r=1,e="",u="",h=[0],p=function(n){1===r&&(n||(e=e.replace(/^\s*\n\s*|\s*\n\s*$/g,"")))?h.push(0,n,e):3===r&&(n||e)?(h.push(3,n,e),r=2):2===r&&"..."===e&&n?h.push(4,n,0):2===r&&e&&!n?h.push(5,0,!0,e):r>=5&&((e||!n&&5===r)&&(h.push(r,0,e,s),r=6),n&&(h.push(r,n,0,s),r=6)),e="";},a=0;a<n.length;a++){a&&(1===r&&p(),p(a));for(var l=0;l<n[a].length;l++)t=n[a][l],1===r?"<"===t?(p(),h=[h],r=3):e+=t:4===r?"--"===e&&">"===t?(r=1,e=""):e=t+e[0]:u?t===u?u="":e+=t:'"'===t||"'"===t?u=t:">"===t?(p(),r=1):r&&("="===t?(r=5,s=e,e=""):"/"===t&&(r<5||">"===n[a][l+1])?(p(),3===r&&(h=h[0]),r=h,(h=h[0]).push(2,0,r),r=0):" "===t||"\t"===t||"\n"===t||"\r"===t?(p(),r=2):e+=t),3===r&&"!--"===e&&(r=4,h=h[0]);}return p(),h}(s)),r),arguments,[])).length>1?r:r[0]} |
||||
|
||||
export default htm_module; |
@ -0,0 +1,14 @@
@@ -0,0 +1,14 @@
|
||||
{ |
||||
"imports": { |
||||
"@joeattardi/emoji-button": "./@joeattardi/emoji-button.js", |
||||
"@justinribeiro/lite-youtube": "./@justinribeiro/lite-youtube.js", |
||||
"@videojs/http-streaming/dist/videojs-http-streaming.min.js": "./@videojs/http-streaming/dist/videojs-http-streaming.min.js", |
||||
"@videojs/themes/fantasy/index.css": "./@videojs/themes/fantasy/index.css", |
||||
"htm": "./htm.js", |
||||
"preact": "./preact.js", |
||||
"showdown": "./showdown.js", |
||||
"tailwindcss/dist/tailwind.min.css": "./tailwindcss/dist/tailwind.min.css", |
||||
"video.js/dist/video-js.min.css": "./videojs/dist/video-js.min.css", |
||||
"video.js/dist/video.min.js": "./videojs/dist/video.min.js" |
||||
} |
||||
} |
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Loading…
Reference in new issue