mirror of
https://github.com/ragestudio/comty.git
synced 2025-06-09 18:44:16 +00:00
use html
as layout root
This commit is contained in:
parent
154d977ebd
commit
5f2a532a1a
@ -1,4 +1,4 @@
|
||||
#root {
|
||||
html {
|
||||
&.mobile {
|
||||
.playlist_view {
|
||||
display: flex;
|
||||
|
@ -1,6 +1,6 @@
|
||||
@import "theme/vars.less";
|
||||
|
||||
#root {
|
||||
html {
|
||||
&.mobile {
|
||||
&.page-panel-spacer {
|
||||
.pagePanels {
|
||||
|
@ -1,4 +1,4 @@
|
||||
#root {
|
||||
html {
|
||||
&.mobile {
|
||||
.player-controls {
|
||||
svg {
|
||||
|
@ -1,6 +1,6 @@
|
||||
@top_controls_height: 55px;
|
||||
|
||||
#root {
|
||||
html {
|
||||
&.mobile {
|
||||
.embbededMediaPlayerWrapper {
|
||||
.player {
|
||||
|
@ -1,6 +1,6 @@
|
||||
@import "theme/vars.less";
|
||||
|
||||
#root {
|
||||
html {
|
||||
&.mobile {
|
||||
.post-list {
|
||||
overflow: unset;
|
||||
|
@ -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);
|
||||
|
@ -1,6 +1,6 @@
|
||||
@import "theme/vars.less";
|
||||
|
||||
#root {
|
||||
html {
|
||||
&.mobile {
|
||||
.userCard {
|
||||
width: 100%;
|
||||
|
157
packages/app/src/cores/stream/BufferedStreamReader.js
Normal file
157
packages/app/src/cores/stream/BufferedStreamReader.js
Normal 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;
|
||||
}
|
||||
}
|
87
packages/app/src/cores/stream/stream.core.js
Normal file
87
packages/app/src/cores/stream/stream.core.js
Normal 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) {
|
||||
|
||||
}
|
||||
}
|
146
packages/app/src/cores/stream/streamPlayer.js
Normal file
146
packages/app/src/cores/stream/streamPlayer.js
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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")
|
||||
|
@ -1,7 +1,7 @@
|
||||
@panel-width: 500px;
|
||||
@chatbox-header-height: 50px;
|
||||
|
||||
#root {
|
||||
html {
|
||||
&.mobile {
|
||||
.livestream {
|
||||
flex-direction: column;
|
||||
|
@ -1,4 +1,4 @@
|
||||
#root {
|
||||
html {
|
||||
&.mobile {
|
||||
.playlist_view {
|
||||
display: flex;
|
||||
|
@ -1,6 +1,6 @@
|
||||
@import "theme/vars.less";
|
||||
|
||||
#root {
|
||||
html {
|
||||
&.mobile {
|
||||
&.centered-content {
|
||||
.app_layout {
|
||||
|
Loading…
x
Reference in New Issue
Block a user