Skip to content

Commit 42068c1

Browse files
committed
feat: audio element
1 parent 6d60739 commit 42068c1

26 files changed

Lines changed: 2448 additions & 182 deletions

File tree

apps/demo/src/app.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ declare const jp, GDPerformanceMonitor, android, java, UIColor;
9292
let monitor;
9393
import { Application, path as filePath, knownFolders, Utils, path as nsPath, ImageSource, Trace, Screen, Color } from '@nativescript/core';
9494

95-
Canvas.useSurface = false;
95+
// Canvas.useSurface = false;
9696
// Canvas.forceGL = false;
9797
Application.on('discardedError', (args) => {
9898
console.log('discardedError', args.error, args);

apps/demo/src/plugin-demos/canvas-media.xml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@
1313
type="video/mp4"/>
1414
</video:Video>
1515

16-
<!-- <ui:Audio margin="0 10" controls="true" row="1" playsinline="true" height="300">
17-
<ui:source src="https://interactive-examples.mdn.mozilla.net/media/cc0-audio/t-rex-roar.mp3"
16+
<audio:Audio margin="0 10" controls="true" row="1" playsinline="true">
17+
<ui:Source src="https://samplelib.com/mp3/sample-15s.mp3"
1818
type="audio/mp3"/>
19-
</ui:Audio> -->
19+
</audio:Audio>
2020
</GridLayout>
2121
</Page>

packages/canvas-media/audio/common.ts

Lines changed: 90 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,94 @@ import { MediaBase } from '../common';
33

44
@CSSType('Audio')
55
export abstract class AudioBase extends MediaBase {
6-
public controls: boolean;
7-
public loop: boolean;
8-
public autoPlay: boolean;
6+
public abstract controls: boolean;
7+
public abstract loop: boolean;
8+
public abstract autoplay: boolean;
9+
10+
public static playingEvent = 'playing';
11+
public static playEvent = 'play';
12+
public static pauseEvent = 'pause';
13+
public static canplayEvent = 'canplay';
14+
public static canplaythroughEvent = 'canplaythrough';
15+
public static timeupdateEvent = 'timeupdate';
16+
public static durationchangeEvent = 'durationchange';
17+
public static loadedmetadataEvent = 'loadedmetadata';
18+
public static loadeddataEvent = 'loadeddata';
19+
public static endedEvent = 'ended';
20+
public static errorEvent = 'error';
21+
22+
public static readonly HAVE_NOTHING = 0;
23+
public static readonly HAVE_METADATA = 1;
24+
public static readonly HAVE_CURRENT_DATA = 2;
25+
public static readonly HAVE_FUTURE_DATA = 3;
26+
public static readonly HAVE_ENOUGH_DATA = 4;
27+
28+
abstract play(): Promise<void>;
29+
abstract pause(): void;
30+
abstract load(): void;
31+
32+
_capturedListeners = {} as Record<string, Function[]>;
33+
_listeners = {} as Record<string, Function[]>;
34+
35+
addEventListener(type: string, listener: Function, useCapture?: boolean | any) {
36+
if (!this._listeners) this._listeners = {};
37+
let isCapture = false;
38+
if (typeof useCapture === 'boolean') isCapture = useCapture;
39+
else if (typeof useCapture === 'object') isCapture = useCapture?.capture ?? false;
40+
41+
const target = isCapture ? this._capturedListeners : this._listeners;
42+
if (!this._listenerExist(type, listener, target)) {
43+
if (Array.isArray(target[type])) {
44+
target[type].push(listener);
45+
} else {
46+
target[type] = [listener];
47+
}
48+
}
49+
}
50+
51+
removeEventListener(type: string, listener: Function, useCapture?: boolean | any) {
52+
let isCapture = false;
53+
if (typeof useCapture === 'boolean') isCapture = useCapture;
54+
else if (typeof useCapture === 'object') isCapture = useCapture?.capture ?? false;
55+
const target = isCapture ? this._capturedListeners : this._listeners;
56+
this._removeListener(type, listener, target);
57+
}
58+
59+
_notifyListener(type: string) {
60+
const captured = this._capturedListeners?.[type];
61+
if (Array.isArray(captured)) {
62+
for (let i = 0; i < captured.length; i++) {
63+
const cb = captured[i];
64+
if (typeof cb === 'function') cb();
65+
}
66+
}
67+
68+
const events = this._listeners?.[type];
69+
if (Array.isArray(events)) {
70+
for (let i = 0; i < events.length; i++) {
71+
const cb = events[i];
72+
if (typeof this[type] === 'function') {
73+
try {
74+
(this as any)[type]();
75+
} catch (e) {}
76+
}
77+
if (typeof cb === 'function') cb();
78+
}
79+
}
80+
}
81+
82+
_removeListener(type: string, listener: Function, listenersObj: any) {
83+
if (!listenersObj) return;
84+
const arr = listenersObj[type];
85+
if (!Array.isArray(arr)) return;
86+
const idx = arr.indexOf(listener as any);
87+
if (idx > -1) arr.splice(idx, 1);
88+
}
89+
90+
_listenerExist(type: string, listener: Function, listenersObj: any): boolean {
91+
if (!listenersObj) return false;
92+
const arr = listenersObj[type];
93+
if (!Array.isArray(arr)) return false;
94+
return arr.indexOf(listener as any) !== -1;
95+
}
996
}
Lines changed: 250 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,251 @@
1-
import {AudioBase} from "./common";
1+
import { AudioBase } from './common';
2+
import { Application, booleanConverter, knownFolders, path } from '@nativescript/core';
3+
import { Source } from '..';
4+
import { durationProperty, currentTimeProperty } from '../common';
25

3-
export class Audio extends AudioBase {}
6+
declare var org: any;
7+
8+
export class Audio extends AudioBase {
9+
_instance: any;
10+
_playResolve: Function | null = null;
11+
_playReject: Function | null = null;
12+
_playPromise: Promise<void> | null = null;
13+
_sourceView: Source[] = [];
14+
_playing: boolean = false;
15+
_readyState: number = 0;
16+
_isCustom: boolean = false;
17+
18+
static createCustomView() {
19+
const audio = new Audio();
20+
audio._isCustom = true;
21+
audio.width = 300;
22+
audio.height = 40;
23+
return audio;
24+
}
25+
26+
constructor() {
27+
super();
28+
29+
const activity: any = Application.android.foregroundActivity || Application.android.startActivity;
30+
this._instance = new org.nativescript.canvas.media.AudioHelper(activity, knownFolders.documents().path);
31+
32+
const ref = new WeakRef(this);
33+
34+
this._instance.setCallback(
35+
new org.nativescript.canvas.media.AudioHelper.Callback({
36+
onDurationChange(duration: number) {
37+
const owner = ref.get();
38+
if (owner) {
39+
let secs = Number(duration) > 0 ? Number(duration) / 1000.0 : NaN;
40+
durationProperty.nativeValueChange(owner, secs);
41+
owner._notifyListener(Audio.durationchangeEvent);
42+
}
43+
},
44+
45+
onPlaying() {
46+
const owner = ref.get();
47+
if (owner) {
48+
owner._playing = true;
49+
if (owner._playResolve) {
50+
owner._playResolve();
51+
owner._playResolve = null;
52+
owner._playReject = null;
53+
owner._playPromise = null;
54+
}
55+
owner._notifyListener(Audio.playingEvent);
56+
}
57+
},
58+
59+
onCurrentTimeChanged(time: number) {
60+
const owner = ref.get();
61+
if (owner) {
62+
let secs = Number(time) / 1000.0;
63+
currentTimeProperty.nativeValueChange(owner, secs);
64+
owner._notifyListener(Audio.timeupdateEvent);
65+
}
66+
},
67+
68+
onLoadedData() {
69+
const owner = ref.get();
70+
if (owner) {
71+
owner._readyState = Audio.HAVE_CURRENT_DATA;
72+
owner._notifyListener(Audio.durationchangeEvent);
73+
owner._notifyListener(Audio.loadedmetadataEvent);
74+
owner._notifyListener(Audio.loadeddataEvent);
75+
}
76+
},
77+
78+
onCanPlay() {
79+
const owner = ref.get();
80+
if (owner) {
81+
owner._notifyListener(Audio.canplayEvent);
82+
}
83+
},
84+
85+
onCanPlayThrough() {
86+
const owner = ref.get();
87+
if (owner) {
88+
owner._notifyListener(Audio.canplaythroughEvent);
89+
}
90+
},
91+
92+
onError(message: string) {
93+
const owner = ref.get();
94+
if (owner) {
95+
owner._playing = false;
96+
if (owner._playReject) {
97+
owner._playReject(new Error(message ?? 'Playback error'));
98+
}
99+
owner._playResolve = null;
100+
owner._playReject = null;
101+
owner._playPromise = null;
102+
owner._notifyListener(Audio.errorEvent);
103+
}
104+
},
105+
}),
106+
);
107+
}
108+
109+
createNativeView() {
110+
return this._instance.getContainer();
111+
}
112+
113+
play() {
114+
if (this._playing) {
115+
return Promise.resolve();
116+
}
117+
118+
if (this._playPromise) {
119+
return this._playPromise;
120+
}
121+
122+
this._playPromise = new Promise<void>((resolve, reject) => {
123+
this._playResolve = resolve;
124+
this._playReject = reject;
125+
try {
126+
this._instance.play();
127+
} catch (e) {
128+
this._playResolve = null;
129+
this._playReject = null;
130+
this._playPromise = null;
131+
reject(e);
132+
}
133+
});
134+
return this._playPromise;
135+
}
136+
137+
pause() {
138+
if (!this._instance) {
139+
return;
140+
}
141+
this._instance.pause();
142+
}
143+
144+
get muted() {
145+
return this._instance.getMuted();
146+
}
147+
148+
set muted(value: boolean) {
149+
this._instance.setMuted(booleanConverter(value));
150+
}
151+
152+
get duration() {
153+
try {
154+
const d = this._instance.getDuration();
155+
if (d == null || Number(d) <= 0) return NaN;
156+
return Number(d) / 1000.0;
157+
} catch (e) {
158+
return NaN;
159+
}
160+
}
161+
162+
get currentTime() {
163+
try {
164+
return this._instance.getCurrentTime();
165+
} catch (e) {
166+
return 0;
167+
}
168+
}
169+
170+
set currentTime(value: number) {
171+
try {
172+
this._instance.setCurrentTime(value);
173+
} catch (e) {}
174+
}
175+
176+
get src() {
177+
try {
178+
return this._instance.getSrc();
179+
} catch (e) {
180+
return undefined;
181+
}
182+
}
183+
184+
set src(value: string) {
185+
try {
186+
if (typeof value === 'string' && value.startsWith('~/')) {
187+
value = path.join(knownFolders.currentApp().path, value.replace('~', ''));
188+
}
189+
this._instance.setSrc(value);
190+
} catch (e) {}
191+
}
192+
193+
load() {
194+
if (!this._instance) {
195+
return;
196+
}
197+
this._instance.load();
198+
}
199+
200+
canPlayType(type: string) {
201+
if (!this._instance) {
202+
return '';
203+
}
204+
return this._instance.canPlayType(type);
205+
}
206+
207+
get autoplay() {
208+
try {
209+
return this._instance.getAutoplay();
210+
} catch (e) {
211+
return false;
212+
}
213+
}
214+
215+
set autoplay(value: boolean) {
216+
this._instance.setAutoplay(booleanConverter(value));
217+
}
218+
219+
get controls() {
220+
try {
221+
return this._instance.getControls();
222+
} catch (e) {
223+
return false;
224+
}
225+
}
226+
227+
set controls(enabled: boolean) {
228+
this._instance.setControls(booleanConverter(enabled));
229+
}
230+
231+
get loop() {
232+
return this._instance.getLoop();
233+
}
234+
235+
set loop(value: boolean) {
236+
this._instance.setLoop(booleanConverter(value));
237+
}
238+
239+
_addChildFromBuilder(name: string, value: any) {
240+
if (value instanceof Source) {
241+
this._sourceView.push(value);
242+
}
243+
}
244+
245+
onLoaded() {
246+
super.onLoaded();
247+
if (this._sourceView.length > 0) {
248+
this.src = this._sourceView[0].src;
249+
}
250+
}
251+
}

0 commit comments

Comments
 (0)