snapshot of https://github.com/pion/webrtc.git (main, 2026-09-15; blobs > 131072B omitted)
Some checks failed
REUSE Compliance Check / lint (push) Failing after 0s
Browser E2E / Test (push) Failing after 3s
Test / test (1.24) (push) Failing after 0s
Test / test-i386 (1.25) (push) Failing after 0s
Test / test-i386 (1.24) (push) Failing after 0s
Test / test-windows (1.25) (push) Failing after 0s
Test / test-windows (1.24) (push) Failing after 0s
Test / test (1.25) (push) Failing after 0s
Examples Tests / Test (push) Failing after 3s
Test / test-macos (1.25) (push) Failing after 0s
Test / test-macos (1.24) (push) Failing after 0s
Go mod tidy / tidy (push) Failing after 0s
Test / test-wasm (push) Failing after 0s
CodeQL / analyze (push) Failing after 1s
Fuzz / fuzz (push) Failing after 0s
Some checks failed
REUSE Compliance Check / lint (push) Failing after 0s
Browser E2E / Test (push) Failing after 3s
Test / test (1.24) (push) Failing after 0s
Test / test-i386 (1.25) (push) Failing after 0s
Test / test-i386 (1.24) (push) Failing after 0s
Test / test-windows (1.25) (push) Failing after 0s
Test / test-windows (1.24) (push) Failing after 0s
Test / test (1.25) (push) Failing after 0s
Examples Tests / Test (push) Failing after 3s
Test / test-macos (1.25) (push) Failing after 0s
Test / test-macos (1.24) (push) Failing after 0s
Go mod tidy / tidy (push) Failing after 0s
Test / test-wasm (push) Failing after 0s
CodeQL / analyze (push) Failing after 1s
Fuzz / fuzz (push) Failing after 0s
This commit is contained in:
144
examples/play-from-disk-playlist-control/web/app.css
Normal file
144
examples/play-from-disk-playlist-control/web/app.css
Normal file
@@ -0,0 +1,144 @@
|
||||
/* SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
margin: 1.5rem;
|
||||
color: #121212;
|
||||
}
|
||||
h2 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
code {
|
||||
background: #eef1f7;
|
||||
padding: 0.1rem 0.35rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
input[type="text"] {
|
||||
padding: 0.5rem;
|
||||
min-width: 220px;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: #0d6efd;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: #0b5ed7;
|
||||
}
|
||||
|
||||
.player {
|
||||
display: grid;
|
||||
grid-template-columns: 320px 1fr;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
audio {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: white;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.logs {
|
||||
min-height: 180px;
|
||||
max-height: 320px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.list li {
|
||||
padding: 0.35rem 0;
|
||||
border-bottom: 1px solid #eceff4;
|
||||
}
|
||||
|
||||
.list li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.list .current {
|
||||
font-weight: bold;
|
||||
color: #0d6efd;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 0.85rem;
|
||||
color: #5a6572;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.track {
|
||||
font-size: 1.2rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.artist {
|
||||
color: #5a6572;
|
||||
}
|
||||
|
||||
.meta {
|
||||
color: #5a6572;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
@media (max-width: 820px) {
|
||||
body {
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
.player {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 540px) {
|
||||
.controls {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
input[type="text"] {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
208
examples/play-from-disk-playlist-control/web/app.js
Normal file
208
examples/play-from-disk-playlist-control/web/app.js
Normal file
@@ -0,0 +1,208 @@
|
||||
/* eslint-env browser */
|
||||
|
||||
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
let pc = null
|
||||
let playlistChannel = null
|
||||
let started = false
|
||||
|
||||
const logs = document.getElementById('logs')
|
||||
const nowPlayingEl = document.getElementById('nowPlaying')
|
||||
const playlistEl = document.getElementById('playlist')
|
||||
const startButton = document.getElementById('startButton')
|
||||
const audio = document.getElementById('remoteAudio')
|
||||
|
||||
const log = msg => {
|
||||
logs.innerHTML += `${msg}<br>`
|
||||
logs.scrollTop = logs.scrollHeight
|
||||
}
|
||||
|
||||
async function startSession () {
|
||||
if (started) {
|
||||
return
|
||||
}
|
||||
started = true
|
||||
startButton.disabled = true
|
||||
log('Creating PeerConnection...')
|
||||
|
||||
pc = new RTCPeerConnection({
|
||||
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
|
||||
})
|
||||
|
||||
pc.createDataChannel('sctp-bootstrap')
|
||||
pc.oniceconnectionstatechange = () => log(`ICE state: ${pc.iceConnectionState}`)
|
||||
pc.onconnectionstatechange = () => log(`Peer state: ${pc.connectionState}`)
|
||||
pc.ontrack = event => {
|
||||
audio.srcObject = event.streams[0]
|
||||
audio.play().catch(() => {})
|
||||
}
|
||||
pc.ondatachannel = event => {
|
||||
if (event.channel.label !== 'playlist') {
|
||||
return
|
||||
}
|
||||
playlistChannel = event.channel
|
||||
playlistChannel.onopen = () => log('playlist DataChannel open')
|
||||
playlistChannel.onclose = () => log('playlist DataChannel closed')
|
||||
playlistChannel.onmessage = e => handleMessage(e.data)
|
||||
}
|
||||
|
||||
pc.addTransceiver('audio', { direction: 'recvonly' })
|
||||
|
||||
try {
|
||||
const offer = await pc.createOffer()
|
||||
await pc.setLocalDescription(offer)
|
||||
log(`Sending offer (${pc.localDescription.sdp.length} bytes)`)
|
||||
|
||||
const res = await fetch('/whep', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/sdp' },
|
||||
body: pc.localDescription.sdp
|
||||
})
|
||||
if (!res.ok) {
|
||||
const body = await res.text()
|
||||
throw new Error(`whep failed: ${res.status} ${body}`)
|
||||
}
|
||||
const answerSDP = await res.text()
|
||||
if (!answerSDP) {
|
||||
throw new Error('no SDP answer from server')
|
||||
}
|
||||
await pc.setRemoteDescription({ type: 'answer', sdp: answerSDP })
|
||||
log('Answer applied. Waiting for media and playlist...')
|
||||
} catch (err) {
|
||||
log(`Error during negotiation: ${err}`)
|
||||
}
|
||||
}
|
||||
|
||||
function sendPrev () {
|
||||
sendRawCommand('prev')
|
||||
}
|
||||
|
||||
function sendNext () {
|
||||
sendRawCommand('next')
|
||||
}
|
||||
|
||||
function sendList () {
|
||||
sendRawCommand('list')
|
||||
}
|
||||
|
||||
function sendCommand () {
|
||||
const value = document.getElementById('commandInput').value
|
||||
if (value.trim() === '') {
|
||||
return
|
||||
}
|
||||
sendRawCommand(value)
|
||||
}
|
||||
|
||||
function sendRawCommand (text) {
|
||||
if (!playlistChannel || playlistChannel.readyState !== 'open') {
|
||||
log('playlist channel not open yet')
|
||||
return
|
||||
}
|
||||
|
||||
playlistChannel.send(text)
|
||||
}
|
||||
|
||||
function handleMessage (data) {
|
||||
const lines = data.trim().split('\n')
|
||||
const playlist = []
|
||||
let current = null
|
||||
let now = null
|
||||
|
||||
lines.forEach(line => {
|
||||
const parts = line.split('|')
|
||||
if (parts.length === 0) {
|
||||
return
|
||||
}
|
||||
switch (parts[0]) {
|
||||
case 'playlist':
|
||||
current = Number(parts[1] || 0)
|
||||
break
|
||||
case 'track':
|
||||
playlist.push({
|
||||
index: Number(parts[1] || 0),
|
||||
serial: Number(parts[2] || 0),
|
||||
duration_ms: Number(parts[3] || 0),
|
||||
title: parts[4] || '',
|
||||
artist: parts[5] || ''
|
||||
})
|
||||
break
|
||||
case 'now':
|
||||
now = {
|
||||
index: Number(parts[1] || 0),
|
||||
serial: Number(parts[2] || 0),
|
||||
channels: Number(parts[3] || 0),
|
||||
sample_rate: Number(parts[4] || 0),
|
||||
duration_ms: Number(parts[5] || 0),
|
||||
title: parts[6] || '',
|
||||
artist: parts[7] || '',
|
||||
vendor: parts[8] || '',
|
||||
comments: (parts[9] || '').split(',').filter(Boolean).map(s => {
|
||||
const [k, v] = s.split('=')
|
||||
return { key: k, value: v }
|
||||
})
|
||||
}
|
||||
break
|
||||
default:
|
||||
log(`Message: ${line}`)
|
||||
}
|
||||
})
|
||||
|
||||
if (playlist.length > 0) {
|
||||
renderPlaylist({ tracks: playlist, current })
|
||||
}
|
||||
if (now) {
|
||||
renderNowPlaying(now)
|
||||
}
|
||||
}
|
||||
|
||||
function renderPlaylist (message) {
|
||||
playlistEl.innerHTML = ''
|
||||
message.tracks.forEach(track => {
|
||||
const li = document.createElement('li')
|
||||
li.innerText = `${track.index + 1}. ${track.title || '(untitled)'} — ${track.artist || 'unknown artist'} (${prettyDuration(track.duration_ms)})`
|
||||
if (track.index === message.current) {
|
||||
li.classList.add('current')
|
||||
}
|
||||
playlistEl.appendChild(li)
|
||||
})
|
||||
|
||||
if (message.hint) {
|
||||
log(message.hint)
|
||||
}
|
||||
}
|
||||
|
||||
function renderNowPlaying (track) {
|
||||
const title = track.title || '(untitled)'
|
||||
const artist = track.artist || 'unknown artist'
|
||||
const vendor = track.vendor ? `<div class="meta">Vendor: ${track.vendor}</div>` : ''
|
||||
const channels = track.channels || '?'
|
||||
const sampleRate = track.sample_rate || '?'
|
||||
const comments = (track.comments || []).map(c => `<div class="meta">${c.key}: ${c.value}</div>`).join('')
|
||||
|
||||
nowPlayingEl.innerHTML = `
|
||||
<div class="label">Now playing</div>
|
||||
<div class="track">${title}</div>
|
||||
<div class="artist">${artist}</div>
|
||||
<div class="meta">Serial: ${track.serial} | Channels: ${channels} | Sample rate: ${sampleRate}</div>
|
||||
<div class="meta">Duration: ${prettyDuration(track.duration_ms)}</div>
|
||||
${vendor}
|
||||
${comments}
|
||||
`
|
||||
}
|
||||
|
||||
function prettyDuration (ms) {
|
||||
if (!ms || ms < 0) {
|
||||
return 'unknown'
|
||||
}
|
||||
const totalSeconds = Math.round(ms / 1000)
|
||||
const minutes = Math.floor(totalSeconds / 60)
|
||||
const seconds = totalSeconds % 60
|
||||
return `${minutes}:${seconds.toString().padStart(2, '0')}`
|
||||
}
|
||||
|
||||
window.startSession = startSession
|
||||
window.sendPrev = sendPrev
|
||||
window.sendNext = sendNext
|
||||
window.sendList = sendList
|
||||
window.sendCommand = sendCommand
|
||||
45
examples/play-from-disk-playlist-control/web/index.html
Normal file
45
examples/play-from-disk-playlist-control/web/index.html
Normal file
@@ -0,0 +1,45 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
|
||||
SPDX-License-Identifier: MIT
|
||||
-->
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Ogg Playlist over SCTP</title>
|
||||
<link rel="stylesheet" href="app.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h2>Ogg Playlist over RTP, control over SCTP</h2>
|
||||
<p>Server hosts both the WebRTC sender and this page. It streams Opus from <code>playlist.ogg</code>, shares metadata over a DataChannel, and lets you jump between tracks.</p>
|
||||
</header>
|
||||
|
||||
<section class="controls">
|
||||
<button id="startButton" onclick="startSession()">Start Session</button>
|
||||
<input id="commandInput" type="text" placeholder="next | prev | 1 | 2 ..." aria-label="Command">
|
||||
<button onclick="sendCommand()">Send</button>
|
||||
<button onclick="sendPrev()">Prev</button>
|
||||
<button onclick="sendNext()">Next</button>
|
||||
</section>
|
||||
|
||||
<section class="player">
|
||||
<audio id="remoteAudio" controls autoplay></audio>
|
||||
<div id="nowPlaying" class="card">Waiting for playlist...</div>
|
||||
</section>
|
||||
|
||||
<section class="grid">
|
||||
<div>
|
||||
<h3>Playlist</h3>
|
||||
<ul id="playlist" class="card list"></ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Logs</h3>
|
||||
<div id="logs" class="card logs"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user