Skip to content

Commit b84978d

Browse files
committed
chore: 2.0.25
1 parent fcf7293 commit b84978d

38 files changed

Lines changed: 1191 additions & 261 deletions

packages/audio-context/common.ts

Lines changed: 30 additions & 145 deletions
Original file line numberDiff line numberDiff line change
@@ -469,158 +469,43 @@ export interface MediaElementLike extends MediaElementTapProvider {
469469
[k: string]: any;
470470
}
471471

472-
const PLAYER_REF_ = Symbol('[[playerRef]]');
472+
const usedMediaElements: WeakSet<MediaElementLike> = new WeakSet();
473473

474-
export abstract class MediaElementAudioSourceBase {
475-
protected [context_]: BaseAudioContext;
476-
protected [PLAYER_REF_]: MediaElementLike;
477-
protected _internalGain: any = null;
478-
protected _activeSource: any = null;
479-
protected _decoded: any = null;
480-
protected _decodingFor: string | null = null;
481-
protected _wasMuted = false;
482-
protected _originalVolume = 1;
483-
protected _onPlayBound: () => void;
484-
protected _onPauseBound: () => void;
485-
protected _onEndedBound: () => void;
486-
protected _tapNode: any = null;
487-
protected _usingTap = false;
488-
489-
constructor(context: BaseAudioContext, mediaElement: MediaElementLike) {
490-
if (!(context instanceof BaseAudioContext)) throw new TypeError('MediaElementAudioSourceNode: invalid context');
491-
if (!mediaElement || (typeof mediaElement.play !== 'function' && typeof mediaElement.src !== 'string')) {
492-
throw new TypeError('MediaElementAudioSourceNode: requires a media element with `src` and `play()`');
493-
}
494-
this[context_] = context;
495-
this[PLAYER_REF_] = mediaElement;
496-
this._internalGain = this._createGainNode();
497-
498-
if (typeof mediaElement.attachAudioContextTap === 'function') {
499-
try {
500-
const tapNative = mediaElement.attachAudioContextTap((this.context as any).native);
501-
if (tapNative) {
502-
this._tapNode = this._wrapTapNative(tapNative);
503-
if (this._tapNode) {
504-
this._tapNode.connect(this._internalGain);
505-
this._usingTap = true;
506-
}
507-
}
508-
} catch (e) {}
509-
}
510-
511-
this._onPlayBound = () => this._handlePlay();
512-
this._onPauseBound = () => this._handlePause();
513-
this._onEndedBound = () => this._handleEnded();
514-
mediaElement.addEventListener?.('play', this._onPlayBound);
515-
mediaElement.addEventListener?.('pause', this._onPauseBound);
516-
mediaElement.addEventListener?.('ended', this._onEndedBound);
517-
}
518-
protected abstract _wrapTapNative(tapNative: any): { connect(t: any): void; disconnect(t?: any): void; stop?(): void } | null;
474+
export function resolveNativePlayer(mediaElement: MediaElementLike): any {
475+
const m = mediaElement as any;
476+
return m?.helper?.player ?? m?.player ?? m?._player ?? null;
477+
}
519478

520-
get mediaElement(): MediaElementLike {
521-
return this[PLAYER_REF_];
479+
export function assertMediaElementUsable(context: BaseAudioContext, mediaElement: MediaElementLike): void {
480+
if (!(context instanceof BaseAudioContext)) {
481+
throw new TypeError('MediaElementAudioSourceNode: invalid context');
522482
}
523-
524-
get context(): BaseAudioContext {
525-
return this[context_];
483+
if (!mediaElement || (typeof mediaElement.play !== 'function' && typeof mediaElement.src !== 'string')) {
484+
throw new TypeError('MediaElementAudioSourceNode: requires a media element with `src` and `play()`');
526485
}
527-
528-
get _outputNode() {
529-
return this._internalGain;
486+
if (usedMediaElements.has(mediaElement)) {
487+
if (typeof DOMException !== 'undefined') {
488+
throw new DOMException('The media element has already been connected to an AudioContext', 'InvalidStateError');
489+
}
490+
throw new Error('The media element has already been connected to an AudioContext');
530491
}
492+
}
531493

532-
connect(target: any, output?: number, input?: number) {
533-
return this._internalGain.connect(target, output, input);
534-
}
535-
disconnect(target?: any, output?: number, input?: number) {
536-
return this._internalGain.disconnect(target, output, input);
537-
}
494+
export function markMediaElementUsed(mediaElement: MediaElementLike): void {
495+
try {
496+
usedMediaElements.add(mediaElement);
497+
} catch (e) {}
498+
}
538499

539-
disposeMediaElementSource() {
540-
const el = this[PLAYER_REF_];
541-
try {
542-
el.removeEventListener?.('play', this._onPlayBound);
543-
el.removeEventListener?.('pause', this._onPauseBound);
544-
el.removeEventListener?.('ended', this._onEndedBound);
545-
} catch (e) {}
546-
if (this._usingTap) {
547-
try {
548-
el.detachAudioContextTap?.();
549-
} catch (e) {}
550-
try {
551-
this._tapNode?.disconnect?.();
552-
} catch (e) {}
553-
this._tapNode = null;
554-
this._usingTap = false;
555-
}
556-
this._stopActive();
557-
this._restoreVolume();
558-
}
559-
560-
protected abstract _createGainNode(): any;
561-
protected abstract _createBufferSource(buffer: any): any;
562-
protected abstract _decodeFromUrl(src: string): Promise<any>;
563-
564-
protected async _handlePlay() {
565-
if (this._usingTap) return;
566-
const el = this[PLAYER_REF_];
567-
const src = el.src ?? '';
568-
if (!src) return;
569-
try {
570-
if (this._decoded == null || this._decodingFor !== src) {
571-
this._decodingFor = src;
572-
this._decoded = await this._decodeFromUrl(src);
573-
}
574-
this._muteOriginal();
575-
this._stopActive();
576-
const source = this._createBufferSource(this._decoded);
577-
source.loop = !!el.loop;
578-
source.connect(this._internalGain);
579-
source.start();
580-
this._activeSource = source;
581-
} catch (e) {}
582-
}
583-
584-
protected _handlePause() {
585-
if (this._usingTap) return;
586-
this._stopActive();
587-
this._restoreVolume();
588-
}
589-
590-
protected _handleEnded() {
591-
if (this._usingTap) return;
592-
this._stopActive();
593-
this._restoreVolume();
594-
}
595-
596-
protected _stopActive() {
597-
const s = this._activeSource;
598-
this._activeSource = null;
599-
if (!s) return;
600-
try {
601-
s.stop?.();
602-
s.disconnect?.();
603-
} catch (e) {}
604-
}
605-
606-
protected _muteOriginal() {
607-
if (this._wasMuted) return;
608-
const el = this[PLAYER_REF_];
609-
if (typeof el.volume === 'number') {
610-
this._originalVolume = el.volume;
611-
try {
612-
el.volume = 0;
613-
this._wasMuted = true;
614-
} catch (e) {}
615-
}
616-
}
500+
export function unmarkMediaElementUsed(mediaElement: MediaElementLike): void {
501+
try {
502+
usedMediaElements.delete(mediaElement);
503+
} catch (e) {}
504+
}
617505

618-
protected _restoreVolume() {
619-
if (!this._wasMuted) return;
620-
const el = this[PLAYER_REF_];
621-
try {
622-
el.volume = this._originalVolume;
623-
} catch (e) {}
624-
this._wasMuted = false;
506+
export function throwInvalidMediaElement(): never {
507+
if (typeof DOMException !== 'undefined') {
508+
throw new DOMException('Failed to attach a native source for the media element', 'InvalidStateError');
625509
}
510+
throw new Error('Failed to attach a native source for the media element');
626511
}

packages/audio-context/index.android.ts

Lines changed: 116 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,41 @@
11
import { Utils } from '@nativescript/core';
2-
import { AnalyserOptions, AudioContextOptions, AudioNodeBase, AudioParamBase, AudioParamHooks, BaseAudioContext, ConstantSourceOptions, ConvolverOptions, DelayOptions, distanceModelFromNumber, distanceModelToNumber, DistanceModelType, IIRFilterOptions, MediaElementAudioSourceBase, MediaElementLike, panningModelFromNumber, panningModelToNumber, PanningModelType, PannerOptions, PeriodicWaveOptions, StereoPannerOptions, WaveShaperOptions, context_, looksLikePath, native_, nativeCtor_, normalizeSourcePath, resolveLatencyHint, resolvePannerOptions, AudioListenerBase } from './common';
2+
import {
3+
AnalyserOptions,
4+
AudioContextOptions,
5+
AudioNodeBase,
6+
AudioParamBase,
7+
AudioParamHooks,
8+
BaseAudioContext,
9+
ConstantSourceOptions,
10+
ConvolverOptions,
11+
DelayOptions,
12+
distanceModelFromNumber,
13+
distanceModelToNumber,
14+
DistanceModelType,
15+
IIRFilterOptions,
16+
MediaElementLike,
17+
panningModelFromNumber,
18+
panningModelToNumber,
19+
PanningModelType,
20+
PannerOptions,
21+
PeriodicWaveOptions,
22+
StereoPannerOptions,
23+
WaveShaperOptions,
24+
context_,
25+
looksLikePath,
26+
native_,
27+
nativeCtor_,
28+
normalizeSourcePath,
29+
resolveLatencyHint,
30+
resolvePannerOptions,
31+
AudioListenerBase,
32+
makeStubParamHooks,
33+
assertMediaElementUsable,
34+
markMediaElementUsed,
35+
unmarkMediaElementUsed,
36+
resolveNativePlayer,
37+
throwInvalidMediaElement,
38+
} from './common';
339

440
type NativeBaseAudioContext = BaseAudioContext & { native: org.nativescript.audiocontext.AudioContextInstance };
541

@@ -403,42 +439,83 @@ export class AudioBufferSourceNode extends AudioScheduledSourceNode {
403439
set buffer(v: AudioBuffer | null) {
404440
this.native.setBuffer(v?.native ?? (null as never));
405441
}
406-
protected _scheduledDuration(): number {
407-
const b = this.native.getBuffer();
408-
if (!b || this.native.getLoop()) return 0;
409-
try {
410-
return b.getDuration() || 0;
411-
} catch (e) {
412-
return 0;
413-
}
414-
}
415442
}
416443

417-
export class MediaElementAudioSourceNode extends MediaElementAudioSourceBase {
418-
constructor(context: AudioContext, mediaElement: MediaElementLike) {
419-
super(context, mediaElement);
444+
export class MediaElementAudioSourceNode extends AudioNode {
445+
private [native_]: org.nativescript.audiocontext.ExternalPcmSourceNode;
446+
private _mediaElement: MediaElementLike;
447+
private _playbackRateParam: AudioParam | null = null;
448+
449+
constructor(context: AudioContext, mediaElement: MediaElementLike, preExistingNative?: org.nativescript.audiocontext.ExternalPcmSourceNode) {
450+
super(context as unknown as NativeBaseAudioContext);
451+
assertMediaElementUsable(context, mediaElement);
452+
453+
const native = preExistingNative ?? MediaElementAudioSourceNode._createNative(context, mediaElement);
454+
if (!native) throwInvalidMediaElement();
455+
456+
this._mediaElement = mediaElement;
457+
this[native_] = native;
458+
markMediaElementUsed(mediaElement);
459+
}
460+
461+
private static _createNative(context: AudioContext, mediaElement: MediaElementLike): org.nativescript.audiocontext.ExternalPcmSourceNode | null {
462+
const ctxNative: any = context.native;
463+
if (!ctxNative) return null;
464+
const player = resolveNativePlayer(mediaElement);
465+
if (player && typeof ctxNative.createSourceNodeFromMediaPlayer === 'function') {
466+
try {
467+
const node = ctxNative.createSourceNodeFromMediaPlayer(player);
468+
if (node) return node;
469+
} catch (e) {}
470+
}
471+
if (typeof mediaElement.attachAudioContextTap === 'function') {
472+
try {
473+
const node = mediaElement.attachAudioContextTap(ctxNative);
474+
if (node) return node;
475+
} catch (e) {}
476+
}
477+
return null;
420478
}
421479

422480
get context() {
423481
return this[context_] as AudioContext;
424482
}
425483

426-
protected _wrapTapNative(tapNative: any) {
427-
if (!tapNative) return null;
428-
const ctx = this.context;
429-
const node = Object.create(AudioNode.prototype) as AudioNode & { native: org.nativescript.audiocontext.AudioNode };
430-
(node as any)[context_] = ctx;
431-
(node as any).native = tapNative;
432-
return node;
484+
get native() {
485+
return this[native_];
433486
}
434-
protected _createGainNode() {
435-
return this.context.createGain();
487+
488+
get mediaElement(): MediaElementLike {
489+
return this._mediaElement;
436490
}
437-
protected _createBufferSource(buffer: AudioBuffer) {
438-
return this.context.createBufferSource({ buffer });
491+
492+
get playbackRate(): AudioParam | null {
493+
if (this._playbackRateParam) return this._playbackRateParam;
494+
const native: any = this[native_];
495+
try {
496+
const param = typeof native?.getPlaybackRateParam === 'function' ? native.getPlaybackRateParam() : null;
497+
if (param) {
498+
this._playbackRateParam = new AudioParam(nativeCtor_, param);
499+
return this._playbackRateParam;
500+
}
501+
} catch (e) {}
502+
return null;
439503
}
440-
protected _decodeFromUrl(src: string): Promise<AudioBuffer> {
441-
return this.context.decodeAudioData(src);
504+
505+
disposeMediaElementSource() {
506+
const native = this[native_];
507+
const el = this._mediaElement;
508+
try {
509+
const ctxNative: any = (this.context as any)?.native;
510+
if (native && ctxNative && typeof ctxNative.detachSource === 'function') {
511+
ctxNative.detachSource(native);
512+
} else if (typeof el?.detachAudioContextTap === 'function') {
513+
el.detachAudioContextTap();
514+
}
515+
} catch (e) {}
516+
this._playbackRateParam = null;
517+
this[native_] = null as never;
518+
unmarkMediaElementUsed(el);
442519
}
443520
}
444521

@@ -607,6 +684,19 @@ export class AudioContext extends BaseAudioContext {
607684
return new MediaElementAudioSourceNode(this, mediaElement);
608685
}
609686

687+
createSourceNodeFromPlayer(playerNative: any): MediaElementAudioSourceNode | null {
688+
try {
689+
const native: any = this.native;
690+
if (!native || !playerNative || typeof native.createSourceNodeFromMediaPlayer !== 'function') return null;
691+
const tapNative = native.createSourceNodeFromMediaPlayer(playerNative);
692+
if (!tapNative) return null;
693+
const stub: MediaElementLike = { src: '', play() {}, pause() {} };
694+
return new MediaElementAudioSourceNode(this, stub, tapNative);
695+
} catch (e) {
696+
return null;
697+
}
698+
}
699+
610700
get native() {
611701
return this[native_];
612702
}

packages/audio-context/index.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ export class AudioScheduledSourceNode extends AudioNode {
116116

117117
export declare class MediaElementAudioSourceNode extends AudioNode {
118118
readonly mediaElement: MediaElementLike;
119+
readonly playbackRate: AudioParam | null;
119120
disposeMediaElementSource(): void;
120121
}
121122

@@ -181,6 +182,7 @@ export declare class AudioBufferSourceNode extends AudioScheduledSourceNode {
181182
constructor(context: BaseAudioContext, options: { buffer?: AudioBuffer });
182183
loop: boolean;
183184
buffer: AudioBuffer | null;
185+
readonly playbackRate: AudioParam;
184186
}
185187

186188
export declare class AudioContext extends BaseAudioContext {
@@ -204,6 +206,8 @@ export declare class AudioContext extends BaseAudioContext {
204206
createPeriodicWave(real: Float32Array | number[], imag: Float32Array | number[], options?: { disableNormalization?: boolean }): PeriodicWave;
205207
createMediaElementSource(mediaElement: MediaElementLike): MediaElementAudioSourceNode;
206208

209+
createSourceNodeFromPlayer(playerNative: any): AudioNode | null;
210+
207211
readonly state: AudioContextState;
208212
onstatechange: ((ev: { type: 'statechange' }) => void) | null;
209213
addEventListener(type: 'statechange', listener: (ev: { type: 'statechange' }) => void): void;

0 commit comments

Comments
 (0)