Skip to content

Commit

Permalink
Merge pull request #41 from ve1ld/ops/media-player
Browse files Browse the repository at this point in the history
Media Player + Ramayanam Scraper
  • Loading branch information
ks0m1c authored Feb 23, 2024
2 parents df3ea6c + 5e26907 commit f85e65a
Show file tree
Hide file tree
Showing 56 changed files with 2,927 additions and 702 deletions.
5 changes: 5 additions & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env bash

AWS_ACCESS_KEY_ID=secrettunnel
AWS_SECRET_ACCESS_KEY=secrettunnel
AWS_DEFAULT_REGION=ap-southeast-1
7 changes: 6 additions & 1 deletion assets/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,12 @@ let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute(
"content",
);
let liveSocket = new LiveSocket("/live", Socket, {
params: { _csrf_token: csrfToken },
params: { _csrf_token: csrfToken,
locale: Intl.NumberFormat().resolvedOptions().locale,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
timezone_offset: -new Date().getTimezoneOffset(),
session: JSON.parse(localStorage.getItem("session")) || {active: true}
},
hooks: Hooks,
});

Expand Down
237 changes: 237 additions & 0 deletions assets/js/hooks/audio_player.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
/**
* Hooks for audio player.
* */

let nowSeconds = () => Math.round(Date.now() / 1000)
let rand = (min, max) => Math.floor(Math.random() * (max - min) + min)
let isVisible = (el) => !!(el.offsetWidth || el.offsetHeight || el.getClientRects().length > 0)

let execJS = (selector, attr) => {
document.querySelectorAll(selector).forEach(el => liveSocket.execJS(el, el.getAttribute(attr)))
}



AudioPlayer = {
mounted() {
this.playbackBeganAt = null
this.player = this.el.querySelector("audio")
this.currentTime = this.el.querySelector("#player-time")
this.duration = this.el.querySelector("#player-duration")
this.progress = this.el.querySelector("#player-progress")

let enableAudio = () => {
if(this.player.src){
document.removeEventListener("click", enableAudio)
const hasNothingToPlay = this.player.readyState === 0;
if(hasNothingToPlay){
this.player.play().catch(error => null)
this.player.pause()
}
}
}

document.addEventListener("click", enableAudio)

this.player.addEventListener("loadedmetadata", e => {
console.log("Loaded metadata!", {
duration: this.player.duration,
event: e,
})
})

this.el.addEventListener("js:listen_now", () => this.play({sync: true}))
this.el.addEventListener("js:play_pause", () => {
console.log("{play_pause event triggerred} player:", this.player)
// toggle play-pause
if(this.player.paused){
this.play()
}
})

this.handleEvent("initSession", (sess) => {
localStorage.setItem("session", JSON.stringify(sess))
})

this.handleEvent("registerEventsTimeline", params => {
console.log("Register Events Timeline", params);
this.player.eventsTimeline = params.voice_events;
})


/// events handled by audio player::
this.handleEvent("play", (params) => {
const {filePath, isPlaying, elapsed, artist, title} = params;
const beginTime = nowSeconds() - elapsed
this.playbackBeganAt = beginTime
let currentSrc = this.player.src.split("?")[0]
const isLoadedAndPaused = currentSrc === filePath && !isPlaying && this.player.paused;
if(isLoadedAndPaused){
this.play({sync: true})
} else if(currentSrc !== filePath) {
currentSrc = filePath
this.player.src = currentSrc
this.play({sync: true})
}

const isMediaSessionApiSupported = "mediaSession" in navigator;
if(isMediaSessionApiSupported){
navigator.mediaSession.metadata = new MediaMetadata({artist, title})
}
})
this.handleEvent("pause", () => this.pause())
this.handleEvent("stop", () => this.stop())
this.handleEvent("seekTo", params => this.seekTo(params))
},
clearNextTimer(){
clearTimeout(this.nextTimer)
this.nextTimer = null
},
clearProgressTimer() {
console.log("[clearProgressTimer]", {
timer: this.progressTimer,
})
clearInterval(this.progressTimer)
},
play(opts = {}){
console.log("Triggered playback, check params", {
player: this.player,
opts,
})

let {sync} = opts
this.clearNextTimer()

//
this.player.play().then(() => {
if(sync) {
const currentTime = nowSeconds() - this.playbackBeganAt
this.player.currentTime = currentTime;
this.currentTime.innerText = this.formatTime(currentTime);
}
const progressUpdateInterval = 100 // 10fps, comfortable for human eye

if (!this.progressTimer) { // single instance of progress timer
this.progressTimer = setInterval(() => this.updateProgress(), progressUpdateInterval)
}
console.log("[play: ProgressTimer]: ", this.progressTimer)

}, error => {
if(error.name === "NotAllowedError"){
execJS("#enable-audio", "data-js-show")
}
})
},

pause(){
this.clearProgressTimer()
this.player.pause()
},

stop(){
this.clearProgressTimer()
this.player.pause()
this.player.currentTime = 0
this.updateProgress()
this.duration.innerText = ""
this.currentTime.innerText = ""
},
seekTo(params) {
const {
positionS
} = params;

const beginTime = nowSeconds() - positionS
this.playbackBeganAt = beginTime;
this.currentTime.innerText = this.formatTime(positionS);
this.player.currentTime = positionS;
this.updateProgress()
},
/**
* Updates playback progress information.
* */
updateProgress() {
// console.log("[updateProgress]", {
// eventsTimeline: this.player.eventsTimeline
// })

this.emphasizeActiveEvent(this.player.currentTime, this.player.eventsTimeline)

if(isNaN(this.player.duration)) {
console.log("player duration is nan")
return false
}

const shouldStopUpdating = this.player.currentTime > 0 && this.player.paused
if (shouldStopUpdating) {
this.clearProgressTimer()
}

const shouldAutoPlayNextSong = !this.nextTimer && this.player.currentTime >= this.player.duration;
if(shouldAutoPlayNextSong) {
this.clearProgressTimer() // stops progress update
const autoPlayMaxDelay = 1500
this.nextTimer = setTimeout(
// pushes next autoplay song to server:
// FIXME: this shall be added in in the following PRs
() => this.pushEvent("next_song_auto"),
rand(0, autoPlayMaxDelay)
)
return
}
this.progress.style.width = `${(this.player.currentTime / (this.player.duration) * 100)}%`
const durationVal = this.formatTime(this.player.duration);
const currentTimeVal = this.formatTime(this.player.currentTime);
console.log("update progress:", {
player: this.player,
durationVal,
currentTimeVal,
})
this.duration.innerText = durationVal;
this.currentTime.innerText = currentTimeVal;
},

formatTime(seconds) {
return new Date(1000 * seconds).toISOString().substring(11, 19)
},
emphasizeActiveEvent(currentTime, events) {

if (!events) {
console.log("No events found")
return;
}

const currentTimeMs = currentTime * 1000
const activeEvent = events.find(event => currentTimeMs >= event.origin && currentTimeMs < (event.origin + event.duration))
console.log("activeEvent:", {currentTimeMs, activeEvent})

if (!activeEvent) {
console.log("No active event found @ time = ", currentTime)
return;
}

const {
verse_id: verseId
} = activeEvent;

if (!verseId) {
return
}


const classVals = ["bg-orange-500", "border-l-8", "border-black"]

// TODO: this is a pedestrian approach that can be improved significantly:
for (const otherDomNode of document.querySelectorAll('[id*="verse-"]')) {
classVals.forEach(classVal => otherDomNode.classList.remove(classVal))
otherDomNode.classList.remove("bg-orange-500")
}

const targetDomId = `verse-${verseId}`
const node = document.getElementById(targetDomId)
classVals.forEach(classVal => node.classList.add(classVal))
}
}


export default AudioPlayer;
5 changes: 5 additions & 0 deletions assets/js/hooks/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,16 @@ import {
TriggerYouTubeFunction,
} from "./youtube_player.js";
import MiniPlayer from "./mini_player.js";
import AudioPlayer from "./audio_player.js";
import ProgressBar from "./progress_bar.js";


let Hooks = {};
Hooks.ShareQuoteButton = ShareQuoteButton;
Hooks.RenderYouTubePlayer = RenderYouTubePlayer;
Hooks.TriggerYouTubeFunction = TriggerYouTubeFunction;
Hooks.MiniPlayer = MiniPlayer;
Hooks.AudioPlayer = AudioPlayer;
Hooks.ProgressBar = ProgressBar;

export default Hooks;
64 changes: 64 additions & 0 deletions assets/js/hooks/progress_bar.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/**
* Progress Bar hooks intended to sync playback related actions
*
* */
ProgressBar = {
mounted() {
this.el.addEventListener("click", (e) => {
// The progress bar is measure in milliseconds,
// with `min` set at 0 and `max` at the duration of the track.
//
// [--------------------------X-----------]
// 0 | offsetWidth
// |
// [--------------------------] e.target.offsetX
//
// When the user clicks on the progress bar (represented by X), we get
// the position of the click in pixels (e.target.offsetX).
//
// We know that we can express the relationship between pixels
// and milliseconds as:
//
// e.target.offsetX : e.target.offsetWidth = X : max
//
// To find X, we do:
// console.log("check possible positions info:", {
// e.

// })

const {
max: maxTime,
} = this.el.dataset

if (!maxTime) {
console.log("unable to seek position, payload is incorrect")
return
}

const containerNode = document.getElementById("player-progress-container")
const maxPlaybackMs = Number(maxTime)
const maxOffset = containerNode.offsetWidth
const currXOffset = e.offsetX;
const playbackPercentage = (currXOffset / maxOffset)
const positionMs = maxPlaybackMs * playbackPercentage

// Optimistic update
this.el.value = positionMs;

console.log("seek attempt @ positionMs:", {
event: e,
maxOffset,
currXOffset,
playbackPercentage,
positionMs
})
this.pushEventTo("#audio-player-container", "seekToMs", { position_ms: positionMs });

return;
});
},
};


export default ProgressBar;
17 changes: 17 additions & 0 deletions config/dev.exs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,23 @@ config :vyasa, Vyasa.Repo,
show_sensitive_data_on_connection_error: true,
pool_size: 10

# MINIO Object Store API Domain
config :ex_aws,
access_key_id: [{:system, "AWS_ACCESS_KEY_ID"}, :instance_role],
secret_access_key: [{:system, "AWS_SECRET_ACCESS_KEY"}, :instance_role],
region: "ap-southeast-1",
http_client: Vyasa.Medium.Ext.S3Client

config :ex_aws, :s3,
scheme: "http://",
host: "localhost",
port: 9000

config :ex_aws, :retries,
max_attempts: 2,
base_backoff_in_ms: 10,
max_backoff_in_ms: 10_000

# For development, we disable any cache and enable
# debugging and code reloading.
#
Expand Down
3 changes: 3 additions & 0 deletions config/runtime.exs
Original file line number Diff line number Diff line change
Expand Up @@ -114,4 +114,7 @@ if config_env() == :prod do
# config :swoosh, :api_client, Swoosh.ApiClient.Hackney
#
# See https://hexdocs.pm/swoosh/Swoosh.html#module-installation for details.
#
else
Vyasa.Parser.Env.load_file('.env')
end
5 changes: 1 addition & 4 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@ version: '3.8'
services:
postgres:
container_name: postgres_container
build:
context: ./GanapatiSQL
dockerfile: Localfile
#image: postgres
image: postgres
environment:
POSTGRES_USER: ${POSTGRES_USER:-postgres}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-root}
Expand Down
Loading

0 comments on commit f85e65a

Please sign in to comment.