Big progress on core audio/playlist service
This commit is contained in:
@@ -1,9 +1,14 @@
|
||||
import {FileSystemFile, Track, TrackMeta, PlaylistItem} from '../models';
|
||||
import {computed, Signal} from '@angular/core';
|
||||
import {computed, Injectable, Signal} from '@angular/core';
|
||||
import {IndexedArray} from '../lib';
|
||||
import {AnalyserService} from './analyser.service';
|
||||
|
||||
@Injectable({providedIn: 'root'})
|
||||
export class AudioService {
|
||||
protected context: AudioContext;
|
||||
protected context: AudioContext = new AudioContext({
|
||||
sampleRate: 44100
|
||||
});
|
||||
public analyser: AnalyserService = new AnalyserService(this.context);
|
||||
protected playlist: IndexedArray<PlaylistItem> = new IndexedArray();
|
||||
public list: Signal<PlaylistItem[]> = this.playlist.playlist;
|
||||
public current: Signal<PlaylistItem|null> = this.playlist.current;
|
||||
@@ -16,8 +21,6 @@ export class AudioService {
|
||||
})
|
||||
|
||||
constructor() {
|
||||
this.context = new AudioContext();
|
||||
|
||||
this.addItem(new Track(
|
||||
new FileSystemFile('1.mp3', 'audio/1.mp3', 0),
|
||||
new TrackMeta(),
|
||||
@@ -42,53 +45,114 @@ export class AudioService {
|
||||
}
|
||||
|
||||
addItem(track: Track) {
|
||||
this.playlist.add(new PlaylistItem(track, this.context));
|
||||
const item = new PlaylistItem(track, this);
|
||||
this.analyser.connectAudio(item);
|
||||
this.playlist.add(item);
|
||||
}
|
||||
|
||||
removeItem(item: PlaylistItem) {
|
||||
if (item.isPlaying() && item === this.current()) {
|
||||
this.current()?.stop();
|
||||
this.playlist.remove(item);
|
||||
this.current()?.play();
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a track from the current playlist.
|
||||
* If we remove the current track, then, if it was playing, start playing the newly "current" item
|
||||
* @param item
|
||||
*/
|
||||
async removeItem(item: PlaylistItem) {
|
||||
this.playlist.remove(item);
|
||||
|
||||
// If the item was not the currently selected track, then we're done.
|
||||
if (item !== this.current()) return;
|
||||
|
||||
const wasPlaying = item.isPlaying();
|
||||
await item.stop();
|
||||
item.removeMedia(); // Remove the media so that if it is still being downloaded, we stop that request and unblock loading any other tracks
|
||||
|
||||
// If we were playing the removed item, start playing the next item in the playlist (if there is one)
|
||||
if (wasPlaying) await this.current()?.play();
|
||||
}
|
||||
|
||||
async play(): Promise<void> {
|
||||
if (this.context.state === 'suspended') {
|
||||
await this.context.resume();
|
||||
}
|
||||
// Make sure the audio context isn't blocked by the browser (disabled autoplay etc)
|
||||
if (this.context.state === 'suspended') await this.context.resume();
|
||||
|
||||
await this.current()?.play();
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
await this.current()?.stop();
|
||||
}
|
||||
|
||||
async next(): Promise<void> {
|
||||
if (this.current()?.isPlaying()) {
|
||||
await this.current()?.stop();
|
||||
this.playlist.next();
|
||||
await this.current()?.play();
|
||||
} else {
|
||||
this.playlist.next();
|
||||
}
|
||||
}
|
||||
|
||||
async prev(): Promise<void> {
|
||||
if (this.current()?.isPlaying()) {
|
||||
await this.current()?.stop();
|
||||
this.playlist.prev();
|
||||
await this.current()?.play();
|
||||
} else {
|
||||
this.playlist.prev();
|
||||
}
|
||||
this.analyser.start();
|
||||
}
|
||||
|
||||
async pause(): Promise<void> {
|
||||
await this.current()?.pause();
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
this.analyser.stop();
|
||||
await this.current()?.stop();
|
||||
}
|
||||
|
||||
async next(): Promise<void> {
|
||||
const current = this.current();
|
||||
if (!current) return;
|
||||
const wasPlaying = current.isPlaying();
|
||||
|
||||
await current.stop();
|
||||
const next = this.playlist.next(true);
|
||||
|
||||
// If the track we have just moved on from was still doing the initial download, then reset the <audio> element
|
||||
// for that track (which will abort its request) so that the next track request (<audio> element) is not blocked.
|
||||
// This can be very important for slower connections (or if transcoding is slow).
|
||||
if (current.isLoading()) current.removeMedia();
|
||||
|
||||
// If we pressed next while playing a track, autoplay the next track
|
||||
if (wasPlaying) await next?.play();
|
||||
}
|
||||
|
||||
async prev(): Promise<void> {
|
||||
const current = this.current();
|
||||
if (!current) return;
|
||||
const wasPlaying = current.isPlaying();
|
||||
|
||||
await current.stop();
|
||||
const prev = this.playlist.prev();
|
||||
|
||||
// If the track we have just moved on from was still doing the initial load, then abort the loading so that the next track request is not blocked.
|
||||
if (current.isLoading()) current.removeMedia();
|
||||
|
||||
// If we pressed prev while playing a track, autoplay the next track
|
||||
if (wasPlaying) await prev?.play();
|
||||
}
|
||||
|
||||
/**
|
||||
* Move to the next track and start playing it. This is mostly used for auto-advancing to the next track
|
||||
* when the current track has ended.
|
||||
*/
|
||||
async playNext(): Promise<void> {
|
||||
const current = this.current();
|
||||
if (!current) return;
|
||||
|
||||
if (current.isPlaying()) await current.stop(); // Track should have ended anyway, but call stop to handle some edge-cases.
|
||||
const next = this.playlist.next();
|
||||
if (next) await next.play();
|
||||
}
|
||||
|
||||
/**
|
||||
* Start playing the track at position "index" in the playlist.
|
||||
* Deliberately take no action if the track referenced is already the current track.
|
||||
* @param index
|
||||
*/
|
||||
async playIndex(index: number): Promise<void> {
|
||||
const current = this.current();
|
||||
const target = this.playlist.getIndex(index);
|
||||
if (!target || current === target) return;
|
||||
|
||||
await current?.stop();
|
||||
this.playlist.setIndex(index);
|
||||
await target.play();
|
||||
}
|
||||
|
||||
/**
|
||||
* Seek to position in the current track
|
||||
* @param percentage Value between 0 and 100. Values outside this range will be clamped.
|
||||
*/
|
||||
async seekPercentage(percentage: number): Promise<void> {
|
||||
const clampedPercentage = Math.min(Math.max(percentage, 0), 100);
|
||||
const current = this.current();
|
||||
if (current) current.seek(current.duration() * (clampedPercentage / 100))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user