use html as layout root

This commit is contained in:
SrGooglo 2023-07-13 15:59:21 +00:00
parent 154d977ebd
commit 5f2a532a1a
14 changed files with 400 additions and 26 deletions

View File

@ -1,4 +1,4 @@
#root {
html {
&.mobile {
.playlist_view {
display: flex;

View File

@ -1,6 +1,6 @@
@import "theme/vars.less";
#root {
html {
&.mobile {
&.page-panel-spacer {
.pagePanels {

View File

@ -1,4 +1,4 @@
#root {
html {
&.mobile {
.player-controls {
svg {

View File

@ -1,6 +1,6 @@
@top_controls_height: 55px;
#root {
html {
&.mobile {
.embbededMediaPlayerWrapper {
.player {

View File

@ -1,6 +1,6 @@
@import "theme/vars.less";
#root {
html {
&.mobile {
.post-list {
overflow: unset;

View File

@ -1,19 +1,3 @@
#root {
&.mobile {
.badges {
.ant-tag {
padding: 10px;
font-size: 0.9rem;
width: 100%;
svg {
font-size: 1.2rem;
}
}
}
}
}
.badges {
display: grid;
grid-template-columns: repeat(2, 1fr);

View File

@ -1,6 +1,6 @@
@import "theme/vars.less";
#root {
html {
&.mobile {
.userCard {
width: 100%;

View File

@ -0,0 +1,157 @@
/* Audio file chunks read must be buffered before sending to decoder.
* Otherwise, decoder returns white noise for odd (not even) chunk size).
* Skipping/hissing occurs if buffer is too small or if network isn't fast enough.
* Users must wait too long to hear audio if buffer is too large.
*
* Returns Promise that resolves when entire stream is read and bytes queued for decoding
*/
export default class BufferedStreamReader {
onRead; // callback on every read. useful for speed calcs
onBufferFull; // callback when buffer fills or read completes
request; // HTTP request we're reading
buffer; // buffer we're filling
bufferPos = 0; // last filled position in buffer
isRunning;
abortController;
constructor(request, readBufferSize) {
if (!(parseInt(readBufferSize) > 0))
throw Error('readBufferSize not provided');
this.request = request;
this.buffer = new Uint8Array(readBufferSize);
}
abort() {
if (this.abortController) {
this.abortController.abort();
}
this.request = null;
}
seek(time) {
if (!this._controller) {
return;
}
const targetByteOffset = Math.floor((time / this._audioContext.sampleRate) * this._bufferSize);
const targetPosition = Math.floor(targetByteOffset / this._chunkSize);
const targetChunkByteOffset = targetByteOffset % this._chunkSize;
// Set the reader's position to the target position
this._controller.reader.seek(targetPosition);
// Update the internal buffer to start reading from the target chunk and byte offset
this._buffer = this._controller.reader.readChunk(targetChunkByteOffset, this._chunkSize);
// Reset the read position and buffer position
this._readPosition = targetByteOffset;
this._bufferPosition = targetChunkByteOffset;
// Emit an event or invoke a callback to notify the AudioStreamPlayer that the seek operation is complete
// You can define an `onSeek` event or callback in BufferedStreamReader and invoke it here.
}
async read() {
if (this.isRunning) {
return console.warn('cannot start - read in progess.');
}
this.isRunning = true;
return this._start()
.catch(e => {
if (e.name === 'AbortError') {
return;
}
this.abort();
throw e;
})
.finally(_ => this.isRunning = false);
}
async _start() {
this.abortController = ('AbortController' in window) ? new AbortController() : null;
const signal = this.abortController ? this.abortController.signal : null;
const response = await fetch(this.request, { signal });
if (!response.ok) throw Error(response.status + ' ' + response.statusText);
if (!response.body) throw Error('ReadableStream not yet supported in this browser - <a href="https://developer.mozilla.org/en-US/docs/Web/API/Body/body#Browser_Compatibility">browser compatibility</a>');
const reader = response.body.getReader(),
contentLength = response.headers.get('content-length'), // requires CORS access-control-expose-headers: content-length
totalBytes = contentLength ? parseInt(contentLength, 10) : 0;
let totalRead = 0, byte, readBufferPos = 0;
const read = async () => {
const { value, done } = await reader.read();
const byteLength = value ? value.byteLength : 0;
totalRead += byteLength;
if (this.onRead) {
this.onRead({ bytes: value, totalRead, totalBytes, done });
}
// avoid blocking read()
setTimeout(_ => this._readIntoBuffer({ value, done, request: this.request }));
// console.log(this.request);
// this._readIntoBuffer({ value, done });
if (!done) {
return read();
}
};
return read();
}
_requestIsAborted({ request }) {
return this.request !== request;
}
_reset() {
this.bufferPos = 0
this.buffer.fill(0)
}
_flushBuffer({ end, done, request }) {
if (this._requestIsAborted({ request })) {
return
}
this.onBufferFull({ bytes: this.buffer.slice(0, end), done });
}
/* read value into buffer and call onBufferFull when reached */
_readIntoBuffer({ value, done, request }) {
if (this._requestIsAborted({ request })) {
return
}
if (done) {
this._flushBuffer({ end: this.bufferPos, done, request });
return;
}
const src = value,
srcLen = src.byteLength,
bufferLen = this.buffer.byteLength;
let srcStart = 0,
bufferPos = this.bufferPos;
while (srcStart < srcLen) {
const len = Math.min(bufferLen - bufferPos, srcLen - srcStart);
const end = srcStart + len;
this.buffer.set(src.subarray(srcStart, end), bufferPos);
srcStart += len;
bufferPos += len;
if (bufferPos === bufferLen) {
bufferPos = 0;
this._flushBuffer({ end: Infinity, done, request });
}
}
this.bufferPos = bufferPos;
}
}

View File

@ -0,0 +1,87 @@
import Core from "evite/src/core"
import remotes from "comty.js/remotes"
import PlaylistModel from "comty.js/models/playlists"
export default class StreamPlayer extends Core {
static refName = "stream"
static namespace = "stream"
static dependencies = [
"settings"
]
queue = []
prevQueue = []
public = {
start: this.start.bind(this),
playback: {
play: this.playback_play.bind(this),
pause: this.playback_pause.bind(this),
seek: this.playback_seek.bind(this),
},
}
async onInitialize() {
//this.audioContext.resume()
}
playback_play() {
this.streamPlayer.resume()
}
playback_pause() {
this.streamPlayer.pause()
}
playback_seek(time) {
this.streamPlayer.seek(time)
}
async createInstance(payload) {
// if payload is a string its means is a track_id, try to fetch it
if (typeof payload === "string") {
payload = await PlaylistModel.getTrack(payload)
}
const instanceObj = {
streamSource: payload.source.split("//")[1].split("/").slice(2).join("/"),
source: payload.source,
metadata: {
title: payload.title,
album: payload.album,
artist: payload.artist,
cover: payload.cover ?? payload.thumbnail,
...payload.metadata,
},
audioBuffer: this.audioContext.createBuffer(2, this.audioContext.sampleRate * 2, 48000),
media: this.audioContext.createBufferSource(),
}
console.log(instanceObj)
return instanceObj
}
async start(
instance,
{
time = 0
} = {}
) {
instance = await this.createInstance(instance)
this.queue = [instance]
this.prevQueue = []
this.play(this.queue[0], {
time: time,
})
}
async play(instance) {
}
}

View File

@ -0,0 +1,146 @@
import BufferedStreamReader from "./BufferedStreamReader.js"
export default class AudioStreamPlayer {
// these shouldn't change once set
_worker = null
_readBufferSize = 1024 * 100
// these are reset
_sessionId = null // used to prevent race conditions between cancel/starts
_reader = null
buffers = []
constructor(audioContext, readBufferSize) {
this.audioContext = audioContext
this._worker = new Worker("/workers/wav-decoder.js")
this._worker.onerror = (event) => {
this.reset()
}
this._worker.onmessage = this._onWorkerMessage.bind(this)
if (readBufferSize) {
this._readBufferSize = readBufferSize
}
this.instanceSource = this.audioContext.createBufferSource()
this.reset()
}
reset() {
this.audioContext.suspend()
if (this._reader) {
this._reader.abort()
this._reader._reset()
}
if (this._sessionId) {
performance.clearMarks(this._downloadMarkKey);
}
this._sessionId = null;
this._reader = null;
this.buffer = null
}
start(url) {
this.reset()
this._sessionId = performance.now()
performance.mark(this._downloadMarkKey)
const reader = new BufferedStreamReader(new Request(url), this._readBufferSize)
reader.onRead = this._downloadProgress.bind(this)
reader.onBufferFull = this.decode.bind(this)
reader.read().catch((e) => {
console.error(e)
})
this._reader = reader
this.resume()
this.instanceSource.connect(this.audioContext.destination)
this.instanceSource.start(0)
}
pause() {
this.audioContext.suspend()
}
resume() {
this.audioContext.resume()
}
seek(time) {
}
decode({ bytes, done }) {
const sessionId = this._sessionId
this._worker.postMessage({ decode: bytes.buffer, sessionId }, [bytes.buffer])
}
// prevent race condition by checking sessionId
_onWorkerMessage(event) {
const { decoded, sessionId } = event.data;
if (decoded.channelData) {
if (!(this._sessionId && this._sessionId === sessionId)) {
console.log("race condition detected for closed session");
return;
}
this.pushToBuffer(decoded);
}
}
_downloadProgress({ bytes, totalRead, totalBytes, done }) {
//console.log(done, (totalRead/totalBytes*100).toFixed(2) );
}
get _downloadMarkKey() {
return `download-start-${this._sessionId}`;
}
_getDownloadStartTime() {
return performance.getEntriesByName(this._downloadMarkKey)[0].startTime;
}
// outputBuffer() {
// const { src, buffer, numberOfChannels, channelData } = this.audioBufferNodes[this._bufferPosition]
// src.onended = () => {
// this._bufferPosition++
// this.outputBuffer()
// }
// for (let c = 0; c < numberOfChannels; c++) {
// buffer.copyToChannel(channelData[c], c);
// }
// src.buffer = buffer
// src.connect(this.audioContext.destination)
// src.start(0)
// }
pushToBuffer({ channelData, length, numberOfChannels, sampleRate }) {
const buffer = this.audioContext.createBuffer(numberOfChannels, length, sampleRate)
for (let c = 0; c < numberOfChannels; c++) {
buffer.copyToChannel(channelData[c], c);
}
}
}

View File

@ -135,7 +135,7 @@ export default class Layout extends React.PureComponent {
return this.layoutInterface.toggleRootContainerClassname("page-panel-spacer", to)
},
toggleRootContainerClassname: (classname, to) => {
const root = document.getElementById("root")
const root = document.documentElement
if (!root) {
console.error("root not found")

View File

@ -1,7 +1,7 @@
@panel-width: 500px;
@chatbox-header-height: 50px;
#root {
html {
&.mobile {
.livestream {
flex-direction: column;

View File

@ -1,4 +1,4 @@
#root {
html {
&.mobile {
.playlist_view {
display: flex;

View File

@ -1,6 +1,6 @@
@import "theme/vars.less";
#root {
html {
&.mobile {
&.centered-content {
.app_layout {