Skip to content

Instantly share code, notes, and snippets.

@Exadra37
Created December 10, 2019 22:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Exadra37/c10d7258864e85c45017292cfba764b9 to your computer and use it in GitHub Desktop.
Save Exadra37/c10d7258864e85c45017292cfba764b9 to your computer and use it in GitHub Desktop.
Youtube Video Player
let Player = {
player: null,
init(domId, playerId, onReady){
window.onYouTubeIframeAPIReady = () => {
this.onIframeReady(domId, playerId, onReady)
}
let youtubeScriptTag = document.createElement("script")
youtubeScriptTag.src = "//www.youtube.com/iframe_api"
document.head.appendChild(youtubeScriptTag)
},
onIframeReady(domId, playerId, onReady) {
this.player = new YT.Player(domId, {
playerVars: {
// 0 - disallows full screen button
fs: 0,
// 0 - does not show youtube recommendations in the end of the video and will put it on start.
rel: 0
},
height: "100%",
width: "100%",
videoId: playerId,
events: {
"onReady": (event => onReady(event)),
"onStateChange": (event => this.onPlayerStateChange(event))
}
})
},
onPlayerStateChange(event){
},
getCurrentTime(){
return Math.floor(this.player.getCurrentTime() * 1000)
},
seekTo(millsec) {
return this.player.seekTo(millsec / 1000)
},
resume() {
return this.player.playVideo()
},
pause() {
return this.player.pauseVideo()
}
}
export default Player
<h2><%= @video.title %></h2>
<div class="row">
<div class="col-lg-8 col-sm-12 mb-3">
<div class="video-container">
<div class="video-row me">
<%= content_tag :div, id: "video", data: [id: @video.id, player_id: player_id(@video)] do %>
<% end %>
</div>
</div>
</div>
<div class="col-lg-4 col-sm-12">
<div class="tab_container">
<input id="tab1" type="radio" name="tabs" checked>
<label for="tab1" class="bg-dark"><i class="fa-icon fas fa-list-ul"></i><span>Index</span></label>
<input id="tab2" type="radio" name="tabs">
<label for="tab2" class="bg-dark"><i class="fa-icon far fa-edit"></i><span>Notes</span></label>
<input id="tab3" type="radio" name="tabs">
<label for="tab3" class="bg-dark"><i class="fa-icon far fa-newspaper"></i><span>Articles</span></label>
<input id="tab4" type="radio" name="tabs">
<label for="tab4" class="bg-dark"><i class="fa-icon far fa-comments"></i><span>Chats</span></label>
<input id="tab5" type="radio" name="tabs">
<label for="tab5" class="bg-dark"><i class="fa-icon fab fa-readme"></i><span>Reviews</span></label>
<section id="content1" class="tab-content">
<p>Index will be here...</p>
</section>
<section id="content2" class="tab-content">
<div id="take-note">
<textarea id="msg-input" rows="3" class="form-control" placeholder="add a note here... markdown is supported !!!"></textarea>
<button id="msg-submit" class="btn btn-dark form-control" type="submit">Add </button>
</div>
<h4>Notes</h4>
<div class="view-notes">
<div id="msg-container" class="panel-body annotations"></div>
</div>
</section>
<section id="content3">
<div class="view-notes tab-content">
<p>Articles comming soon</p>
</div>
</section>
<section id="content4">
<div class="tab-content">
<p>Chat comming soon...</p>
</div>
</section>
<section id="content5">
<div class="tab-content">
<p>Reviews comming soon...</p>
</div>
</section>
</div>
</div>
</div>
import Player from "./player"
let Video = {
init(socket, element){
if (!element) {
return
}
let playerId = element.getAttribute("data-player-id")
let videoId = element.getAttribute("data-id")
socket.connect()
Player.init(element.id, playerId, () => {
this.onReady(videoId, socket)
})
},
onReady(videoId, socket){
let msgContainer = document.getElementById("msg-container")
let msgInput = document.getElementById("msg-input")
let postButton = document.getElementById("msg-submit")
let vidChannel = socket.channel("videos:" + videoId)
msgInput.addEventListener("focusin", e => {
Player.pause()
})
msgInput.addEventListener("focusout", e => {
Player.resume()
}, true)
postButton.addEventListener("click", e => {
let payload = {body: msgInput.value, at: Player.getCurrentTime()}
vidChannel.push("new_annotation", payload)
.receive("error", e => console.log(e))
msgInput.value = ""
})
vidChannel.on("new_annotation", (resp) => {
vidChannel.params.last_seen_id = resp.id
this.renderAnnotation(msgContainer, resp)
})
msgContainer.addEventListener("click", e => {
e.preventDefault()
let seconds = e.target.getAttribute("data-seek") ||
e.target.parentNode.getAttribute("data-seek")
if (!seconds) {
return
}
Player.seekTo(seconds)
})
vidChannel.join()
.receive("ok", (resp) => {
let ids = resp.annotations.map(ann => ann.id)
if (ids.length > 0) {
vidChannel.params.last_seen_id = Math.max(...ids)
}
this.scheduleMessages(msgContainer, resp.annotations)
})
.receive("error", reason => console.log("join failed", reason))
},
// escapes content to protect against XSS attacks.
esc(str){
let div = document.createElement("div")
div.appendChild(document.createTextNode(str))
return div.innerHTML
},
parseMarkdownToHtml(str) {
//let converter = new showdown.Converter({extensions: ['prettify']})
//converter.setFlavor('github')
let converter = new showdown.Converter()
// TODO:
// → we must sanitize the html we generated from the markdown to prevent XSS attacks.
// → still need to decide if we keep the markdown parser in client side or server side.
// → more info in how to mitigate can be found in https://github.com/showdownjs/showdown/wiki/Markdown's-XSS-Vulnerability-(and-how-to-mitigate-it).
// → this.esc(strg) function cannot be used to mitigate the XSS, once it will ignore all html we pass into it.
return `
<div class="note-body">${converter.makeHtml(str)}</div>
`
},
renderMarkdownToHtml(str) {
let html = this.parseMarkdownToHtml(str)
let div = document.createElement("div")
div.insertAdjacentHTML("afterbegin", html)
return div.innerHTML
},
renderAnnotation(msgContainer, {user, body, at}){
let template = document.createElement("div")
let body_html = this.renderBreadcrumb(body) || this.renderMarkdownToHtml(body)
template.innerHTML = `
${this.renderNoteHeader(user, at)}
${body_html}
`
msgContainer.insertAdjacentHTML('afterbegin', template.innerHTML)
msgContainer.scrollTop = msgContainer.scrollHeight
// https://schier.co/blog/2013/01/07/how-to-re-run-prismjs-on-ajax-content.html
Prism.highlightAll()
},
renderNoteHeader(user, at) {
return `
<div class="note-header"><span><strong>${this.esc(user.username)}</strong> at </span>
<a href="#" data-seek="${this.esc(at)}">[${this.formatTime(at)}]</a>
<span class="video-at">(h:m:s)</span><span><strong>:</span></strong>
</div>
`
},
renderBreadcrumb(body) {
if (body.startsWith("\/b ")) {
body = body.replace("/b ", "")
return `
<p class="note-body"><strong>Breadcrumb:</strong> ${this.esc(body)}</p>
`
}
return ""
},
scheduleMessages(msgContainer, annotations){
setTimeout(() => {
let ctime = Player.getCurrentTime()
let remaining = this.renderAtTime(annotations, ctime, msgContainer)
this.scheduleMessages(msgContainer, remaining)
}, 1000)
},
renderAtTime(annotations, seconds, msgContainer){
return annotations.filter( ann => {
if (ann.at > seconds) {
return true
} else {
this.renderAnnotation(msgContainer, ann)
return false
}
})
},
formatTime(at){
let date = new Date(null)
date.setSeconds(at / 1000)
return date.toISOString().substr(11, 8)
}
}
export default Video
defmodule Rumbl.VideoView do
use Rumbl.Web, :view
# TODO:
# → Paly Id must come from database
def player_id(video) do
~r{^.*(?:youtu\.be/|\w+/|v=)(?<id>[^#&?]*)}
|> Regex.named_captures(video.url)
|> get_in(["id"])
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment