diff --git a/bin/build-player.js b/bin/build-player.js index 63e2bebc..594405ac 100755 --- a/bin/build-player.js +++ b/bin/build-player.js @@ -19,6 +19,7 @@ var order = [ 'playerjs.coffee', 'iframechild.coffee', 'odysee.coffee', + 'whepplayer.coffee', 'streamable.coffee', // iframe embed-based players diff --git a/player/update.coffee b/player/update.coffee index a25b6ad7..0a39aca0 100644 --- a/player/update.coffee +++ b/player/update.coffee @@ -18,6 +18,7 @@ TYPE_MAP = bc: IframeChild bn: IframeChild od: OdyseePlayer + wp: WhepPlayer nv: NicoPlayer window.loadMediaPlayer = (data) -> diff --git a/player/whepplayer.coffee b/player/whepplayer.coffee new file mode 100644 index 00000000..7cd7f2b4 --- /dev/null +++ b/player/whepplayer.coffee @@ -0,0 +1,87 @@ +window.WhepPlayer = class WhepPlayer extends Player + constructor: (data) -> + return new WhepPlayer(data) unless this instanceof WhepPlayer + super(data) + @load(data) + + load: (data) -> + @ready = false + @paused = true + @setMediaProperties(data) + + unless data.meta?.whepURL + u = new URL(data.id) + streemId = u.pathname.split('/').filter(Boolean)[0] + data.meta ?= {} + # Adjust these to whatever your backend expects + data.meta.whepURL = "#{u.origin}/api/whep" + data.meta.streamKey = streemId + + { whepURL, streamKey } = data.meta + + # Create video element + videoEl = document.createElement 'video' + videoEl.autoplay = true + videoEl.muted = true + videoEl.controls = true + + # Use the standard container swapper + holder = removeOld() # jQuery object + holder.empty().append(videoEl) + + @videoEl = videoEl + + # ---- WebRTC setup ---- + pc = new RTCPeerConnection() + pc.addTransceiver 'audio', direction: 'recvonly' + pc.addTransceiver 'video', direction: 'recvonly' + + pc.ontrack = (e) => @videoEl.srcObject = e.streams[0] + + pc.onconnectionstatechange = => + if pc.connectionState is 'connected' + @ready = true + @fire? 'ready' + + pc.createOffer() + .then (offer) => + pc.setLocalDescription offer + fetch whepURL, + method: 'POST' + headers: + Authorization: "Bearer #{streamKey}" + 'Content-Type': 'application/sdp' + body: offer.sdp + .then (r) -> r.text() + .then (answer) -> + pc.setRemoteDescription type: 'answer', sdp: answer + .catch (err) -> + console.error 'WHEP negotiation failed:', err + + @pc = pc + + play: -> + @paused = false + @videoEl?.play?() + + pause: -> + @paused = true + @videoEl?.pause?() + + seekTo: (t) -> + @videoEl?.currentTime = t if @ready + + setVolume: (v) -> + @videoEl?.volume = v if @ready + + getTime: (cb) -> + if @ready and @videoEl? + cb @videoEl.currentTime + else + cb 0 + + getVolume: (cb) -> + if @ready and @videoEl? + cb @videoEl.volume + else + cb VOLUME diff --git a/src/get-info.js b/src/get-info.js index ad741281..0dd0d8e4 100644 --- a/src/get-info.js +++ b/src/get-info.js @@ -281,6 +281,13 @@ var Getters = { }); }, + wp: function (id, callback) { + const streemId = new URL(id).pathname.split('/').filter(Boolean)[0]; + var title = `${streemId}'s WHEP Livestream`; + var media = new Media(id, title, "--:--", "wp"); + callback(false, media); + }, + /* rtmp stream */ rt: function (id, callback) { var title = "Livestream"; diff --git a/www/js/util.js b/www/js/util.js index 674e358a..f2ab21a7 100644 --- a/www/js/util.js +++ b/www/js/util.js @@ -1347,7 +1347,7 @@ function parseMediaLink(url) { if(data.protocol == 'rtmp:') { return { type: 'rt', id: url }; - } + } if (data.pathname.match(/\.m3u8$/)) { return { type: 'hl', id: url }; } @@ -1356,6 +1356,8 @@ function parseMediaLink(url) { } switch(data.hostname.replace('www.', '')){ + case 'streem.vereto.net': + return { type: 'wp', id: url } case 'youtube.com': if(data.pathname == '/watch'){ return { type: 'yt', id: data.searchParams.get('v') }