@christianriedl/media 1.0.1 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,21 +1,36 @@
1
1
  import { IRest } from '@christianriedl/rest';
2
2
  import { ILogger, Dictionary } from '@christianriedl/utils';
3
+ import { EMediaType } from "./iMedia";
4
+ export declare enum EPlayState {
5
+ PowerOff = 0,
6
+ NoMedia = 1,
7
+ Paused = 2,
8
+ Stopped = 3,
9
+ Play = 4,
10
+ PlayAll = 5,
11
+ PlayOnly = 6
12
+ }
3
13
  export interface IPlayerState {
4
14
  playerName: string;
5
15
  albumId: string;
6
16
  mediaId: string;
7
17
  trackNo: number;
8
18
  duration: number;
9
- state: string;
19
+ state: EPlayState;
10
20
  volume: number;
11
21
  mute: boolean;
12
22
  }
13
23
  export interface IPlayerRequest {
14
24
  playerName: string;
15
- folderId: string;
16
- mediaId: string;
17
- trackNo: number;
18
- withStreamTitle: boolean;
25
+ folderId?: string;
26
+ mediaId?: string;
27
+ trackNo?: number;
28
+ withStreamTitle?: boolean;
29
+ }
30
+ export interface IPlayerCommand {
31
+ playerName: string;
32
+ cmd: string;
33
+ value: string;
19
34
  }
20
35
  export declare class PlayerService {
21
36
  rest: IRest;
@@ -24,8 +39,12 @@ export declare class PlayerService {
24
39
  playerNames: string[];
25
40
  playerStates: Dictionary<IPlayerState>;
26
41
  constructor(rest: IRest, log: ILogger);
27
- getPlayers(): Promise<string[]>;
42
+ getPlayers(mediaType?: EMediaType, isReady?: boolean): Promise<string[]>;
28
43
  getPlayerState(playerName: string): Promise<IPlayerState | null>;
29
44
  play(playerRequest: IPlayerRequest): Promise<boolean>;
30
- playStop(playerName: string): Promise<boolean>;
45
+ stop(playerName: string): Promise<boolean>;
46
+ pause(playerName: string): Promise<boolean>;
47
+ next(playerName: string): Promise<boolean>;
48
+ previous(playerName: string): Promise<boolean>;
49
+ command(playerCommand: IPlayerCommand): Promise<boolean>;
31
50
  }
@@ -1,3 +1,13 @@
1
+ export var EPlayState;
2
+ (function (EPlayState) {
3
+ EPlayState[EPlayState["PowerOff"] = 0] = "PowerOff";
4
+ EPlayState[EPlayState["NoMedia"] = 1] = "NoMedia";
5
+ EPlayState[EPlayState["Paused"] = 2] = "Paused";
6
+ EPlayState[EPlayState["Stopped"] = 3] = "Stopped";
7
+ EPlayState[EPlayState["Play"] = 4] = "Play";
8
+ EPlayState[EPlayState["PlayAll"] = 5] = "PlayAll";
9
+ EPlayState[EPlayState["PlayOnly"] = 6] = "PlayOnly";
10
+ })(EPlayState || (EPlayState = {}));
1
11
  export class PlayerService {
2
12
  rest;
3
13
  mediaUrl;
@@ -9,8 +19,13 @@ export class PlayerService {
9
19
  this.rest = rest;
10
20
  this.mediaUrl = rest.serviceUrl;
11
21
  }
12
- async getPlayers() {
13
- const players = await this.rest.getData('apiplayer/players');
22
+ async getPlayers(mediaType, isReady) {
23
+ const query = {};
24
+ if (mediaType)
25
+ query.mediaType = mediaType;
26
+ if (isReady)
27
+ query.isReady = isReady;
28
+ const players = await this.rest.getData('apiplayer/players', query);
14
29
  this.playerNames = players.result;
15
30
  return this.playerNames;
16
31
  }
@@ -27,8 +42,24 @@ export class PlayerService {
27
42
  const result = await this.rest.postData('apiplayer/play', playerRequest);
28
43
  return result.ok;
29
44
  }
30
- async playStop(playerName) {
31
- const result = await this.rest.postData('apiplayer/playstop', { playerName: playerName });
45
+ async stop(playerName) {
46
+ const result = await this.rest.postData('apiplayer/playstop/' + playerName);
47
+ return result.ok;
48
+ }
49
+ async pause(playerName) {
50
+ const result = await this.rest.postData('apiplayer/playpause/' + playerName);
51
+ return result.ok;
52
+ }
53
+ async next(playerName) {
54
+ const result = await this.rest.postData('apiplayer/playnext/' + playerName);
55
+ return result.ok;
56
+ }
57
+ async previous(playerName) {
58
+ const result = await this.rest.postData('apiplayer/playprevious/' + playerName);
59
+ return result.ok;
60
+ }
61
+ async command(playerCommand) {
62
+ const result = await this.rest.postData('apiplayer/playcommand', playerCommand);
32
63
  return result.ok;
33
64
  }
34
65
  }
@@ -1 +1 @@
1
- {"version":3,"file":"playerService.js","sourceRoot":"","sources":["../src/playerService.ts"],"names":[],"mappings":"AA0BA,MAAM,OAAO,aAAa;IACtB,IAAI,CAAQ;IACZ,QAAQ,CAAS;IACjB,GAAG,CAAU;IACb,WAAW,GAAa,EAAE,CAAC;IAC3B,YAAY,GAA6B,EAAE,CAAA;IAE3C,YAAY,IAAW,EAAE,GAAY;QACjC,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,UAAU,CAAC;IACpC,CAAC;IACD,KAAK,CAAC,UAAU;QACZ,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,OAAO,CAAW,mBAAmB,CAAC,CAAC;QACvE,IAAI,CAAC,WAAW,GAAG,OAAO,CAAC,MAAkB,CAAC;QAC9C,OAAO,IAAI,CAAC,WAAW,CAAC;IAC5B,CAAC;IACD,KAAK,CAAC,cAAc,CAAC,UAAkB;QACnC,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,OAAO,CAAe,qBAAqB,EAAE,EAAE,UAAU,EAAE,UAAU,EAAC,CAAC,CAAC;QACvG,IAAI,MAAM,CAAC,EAAE,EAAE;YACX,MAAM,WAAW,GAAG,MAAM,CAAC,MAAsB,CAAC;YAClD,IAAI,CAAC,YAAY,CAAC,UAAU,CAAC,GAAG,WAAW,CAAC;YAC5C,OAAO,WAAW,CAAC;SACtB;QACD,OAAO,IAAI,CAAC;IAChB,CAAC;IACD,KAAK,CAAC,IAAI,CAAC,aAA6B;QACpC,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAiB,gBAAgB,EAAE,aAAa,CAAC,CAAC;QACzF,OAAO,MAAM,CAAC,EAAE,CAAC;IACrB,CAAC;IACD,KAAK,CAAC,QAAQ,CAAC,UAAkB;QAC7B,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAM,oBAAoB,EAAE,EAAE,UAAU,EAAE,UAAU,EAAE,CAAC,CAAC;QAC/F,OAAO,MAAM,CAAC,EAAE,CAAC;IACrB,CAAC;CACJ"}
1
+ {"version":3,"file":"playerService.js","sourceRoot":"","sources":["../src/playerService.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@christianriedl/media",
3
- "version": "1.0.1",
3
+ "version": "1.0.4",
4
4
  "description": "RIC media interfaces",
5
5
 
6
6
  "main": "dist/index.js",
@@ -1,14 +1,18 @@
1
1
  <script setup lang="ts">
2
2
  import { inject, ref, reactive, computed, onMounted, onUnmounted, nextTick, watch } from 'vue';
3
- import { IAppState } from '@christianriedl/utils';
4
- import { EItemType, EMediaType, IMediaFolder, IMediaItem, IAudioFile, MediaService } from '@christianriedl/media';
3
+ import { IAppState, Helper } from '@christianriedl/utils';
4
+ import { EItemType, EMediaType, IMediaFolder, IMediaItem, IAudioFile, MediaService, EPlayState, PlayerService } from '@christianriedl/media';
5
5
 
6
6
  const appState = inject<IAppState>('appstate')!;
7
7
  const getMediaService = inject<() => MediaService>('get-media')!;
8
8
  const mediaService = getMediaService();
9
+ const getPlayerService = inject<() => PlayerService>('get-player')!;
10
+ const playerService = getPlayerService();
9
11
  const heightStyle = computed(() => { return { height: appState.bodyHeight.value + 'px', overflowY: 'auto' } });
10
12
  const isMobile = appState.isMobile;
11
13
 
14
+ const playerNames = reactive<string[]>(['Local']);
15
+ const currentPlayer = ref('Local');
12
16
  const items: IMediaItem[] = reactive([]);
13
17
  const selected = ref<IMediaItem>({ Name: 'Root', ItemType: EItemType.Root } as IMediaFolder);
14
18
  const playingTrackUrl = ref("");
@@ -19,7 +23,10 @@
19
23
  const positionLength = ref(0);
20
24
  const listHeight = ref(0);
21
25
  const playIndex = ref(-1);
26
+ const volume = ref(0);
27
+ const mute = ref(false);
22
28
 
29
+ const isLocal = computed(() => currentPlayer.value == 'Local');
23
30
  const backVisible = computed(() => selected.value.ItemType != EItemType.AudioRoot);
24
31
  const downloadAlbumVisible = computed(() => selected.value.ItemType == EItemType.AudioAlbum && !playingTrackUrl.value && !isMobile);
25
32
  const downloadVisible = computed(() => playingTrackUrl.value && !isMobile);
@@ -35,6 +42,10 @@
35
42
  }
36
43
  return '';
37
44
  });
45
+ let syncTimer = 0;
46
+ let cmdTimer = 0;
47
+ let syncRemote = false;
48
+
38
49
  window.addEventListener('popstate', onPopState);
39
50
  function onPopState (event: any) {
40
51
  if (event.state && event.state.noBackExitsApp && backVisible.value) {
@@ -59,6 +70,8 @@
59
70
  selected.value = root;
60
71
  items.splice(0, items.length, ...root.Folders);
61
72
  computeListHeight();
73
+ const players = await playerService.getPlayers(EMediaType.Audio, true);
74
+ playerNames.splice(playerNames.length, 0, ...players);
62
75
  })
63
76
  watch(appState.bodyHeight, () => computeListHeight());
64
77
  function computeListHeight() {
@@ -97,24 +110,68 @@
97
110
  }
98
111
  }
99
112
  function listBack() {
113
+ syncRemote = false;
114
+ playingTrack.value = null;
100
115
  const parent = mediaService.folders[selected.value.DLNAParentID];
101
116
  selected.value = parent;
102
117
  items.splice(0, items.length, ...parent.Folders);
103
118
  computeListHeight();
104
119
  }
105
120
  function onTimeUpdate() {
106
- position.value = audio.value!.currentTime;
121
+ setPosition(audio.value!.currentTime);
122
+ }
123
+ function setPosition(value: number) {
124
+ position.value = value;
107
125
  if (playingTrack.value && playingTrack.value.Duration > 0)
108
126
  positionLength.value = position.value * 100 / playingTrack.value.Duration;
109
127
  else
110
128
  positionLength.value = 0;
111
129
  }
130
+ function onPlayerChange() {
131
+ if (syncTimer) {
132
+ window.clearTimeout(syncTimer);
133
+ syncTimer = 0;
134
+ }
135
+ if (!isLocal.value) {
136
+ syncTimer = window.setTimeout(() => getPlayerState(), 1000);
137
+ syncRemote = true;
138
+ }
139
+ }
140
+ async function getPlayerState() {
141
+ syncTimer = 0;
142
+ const playerState = await playerService.getPlayerState(currentPlayer.value);
143
+ playerState.state = Helper.stringToEnum(EPlayState, playerState.state);
144
+ paused.value = playerState.state == EPlayState.Paused || playerState.state == EPlayState.Stopped;
145
+ if (!cmdTimer) {
146
+ volume.value = playerState.volume;
147
+ mute.value = playerState.mute;
148
+ }
149
+ if (syncRemote) {
150
+ if (!selected.value || playerState.folderId != selected.value.DLNAID) {
151
+ const album = mediaService.folders[playerState.albumId];
152
+ if (album) {
153
+ const audios = await mediaService.getAudios(album);
154
+ selected.value = audios;
155
+ items.splice(0, items.length, ...audios.Files);
156
+ playItems.splice(0, items.length, ...audios.Files);
157
+ if (playerState.mediaId) {
158
+ playIndex.value = items.findIndex((it) => it.DLNAID == playerState.mediaId);
159
+ if (playIndex.value >= 0)
160
+ playingTrack.value = items[playIndex.value];
161
+ }
162
+ computeListHeight();
163
+ }
164
+ }
165
+ }
166
+ setPosition(playerState.duration);
167
+ syncTimer = window.setTimeout(() => getPlayerState(), 1000);
168
+ }
112
169
  function onEnded() {
113
170
  mediaService.log.trace(`onEnded ${playIndex} ended`);
114
171
  if (playIndex.value < playItems.length - 1)
115
172
  play(playIndex.value + 1);
116
173
  }
117
- function play(index: number) {
174
+ async function play(index: number) {
118
175
  if (items.length == playItems.length && items[0].DLNAParentID == playItems[0].DLNAParentID)
119
176
  itemIndex.value = index;
120
177
  playIndex.value = index;
@@ -122,29 +179,67 @@
122
179
  paused.value = false;
123
180
  playingTrack.value = item;
124
181
  playingTrackUrl.value = mediaService.getAudioUrl(item);
125
- audio.value!.src = playingTrackUrl.value;
126
- audio.value!.load();
127
- audio.value!.play()
128
- .then(() => { mediaService.log.trace(`play ${index}ended`) });
182
+ if (isLocal.value) {
183
+ audio.value!.src = playingTrackUrl.value;
184
+ audio.value!.load();
185
+ audio.value!.play()
186
+ .then(() => { mediaService.log.trace(`play ${index}ended`) });
187
+ }
188
+ else {
189
+ const rc = await playerService.play({
190
+ playerName: currentPlayer.value,
191
+ folderId: selected.value.DLNAID,
192
+ mediaId: playingTrack.value.DLNAID,
193
+ trackNo: playingTrack.value.TrackNo
194
+ });
195
+ }
196
+ }
197
+ async function playFolder() {
198
+ const rc = await playerService.play({
199
+ playerName: currentPlayer.value,
200
+ folderId: selected.value.DLNAID,
201
+ trackNo: -1
202
+ });
203
+ syncRemote = true;
129
204
  }
130
205
  function playpause() {
131
- if (audio.value!.paused) {
132
- paused.value = false;
133
- audio.value!.play()
134
- .then(() => { console.log("play ended") });
206
+ if (isLocal.value) {
207
+ if (audio.value!.paused) {
208
+ paused.value = false;
209
+ audio.value!.play()
210
+ .then(() => { console.log("play ended") });
211
+ }
212
+ else {
213
+ paused.value = true;
214
+ audio.value!.pause();
215
+ }
135
216
  }
136
217
  else {
137
- paused.value = true;
138
- audio.value!.pause();
218
+ if (paused.value) {
219
+ playerService.play({ playerName: currentPlayer.value });
220
+ }
221
+ else {
222
+ playerService.pause(currentPlayer.value);
223
+ }
139
224
  }
140
225
  }
141
226
  function previous() {
142
- if (playIndex.value > 0)
143
- play(playIndex.value - 1);
227
+ if (isLocal.value) {
228
+ if (playIndex.value > 0)
229
+ play(playIndex.value - 1);
230
+ }
231
+ else {
232
+ playerService.previous(currentPlayer.value);
233
+ }
144
234
  }
145
235
  function next() {
146
- if (playIndex.value < playItems.length - 1)
147
- play(playIndex.value + 1);
236
+ if (isLocal.value) {
237
+ if (playIndex.value < playItems.length - 1)
238
+ play(playIndex.value + 1);
239
+ }
240
+ else {
241
+ playerService.next(currentPlayer.value);
242
+ }
148
243
  }
149
244
  function download() {
150
245
  window.open(playingTrackUrl.value);
@@ -153,6 +248,19 @@
153
248
  const downloadUrl = mediaService.getAlbumDownloadUrl(selected.value.Url);
154
249
  window.open(downloadUrl);
155
250
  }
251
+ function changeVolume() {
252
+ if (cmdTimer)
253
+ window.clearTimeout(cmdTimer);
254
+
255
+ cmdTimer = window.setTimeout(async () => {
256
+ await playerService.command({
257
+ playerName: currentPlayer.value,
258
+ cmd: "Volume",
259
+ value: volume.value.toString()
260
+ });
261
+ cmdTimer = 0;
262
+ }, 500);
263
+ }
156
264
  function to_time(s: number): string {
157
265
  var r = "";
158
266
  s = Math.floor(s);
@@ -181,14 +289,28 @@
181
289
  <audio ref="audio" preload="none" @ended="onEnded" @timeupdate="onTimeUpdate">
182
290
  </audio>
183
291
  <v-list-item three-line>
184
- <v-list-item-avatar tile rounded="0" size="x-large" v-if="selected.ThumbnailUrl">
185
- <img width="40" :src="selected.ThumbnailUrl">
186
- </v-list-item-avatar>
187
- <v-list-item-content>
188
- <v-list-item-title>{{selected.title}}</v-list-item-title>
189
- <v-list-item-subtitle>{{selected.subTitle}}</v-list-item-subtitle>
190
- <v-list-item-subtitle v-if="playingTrack">{{playingTrack.Name}}</v-list-item-subtitle>
191
- </v-list-item-content>
292
+ <v-row>
293
+ <v-col cols="1">
294
+ <v-list-item-avatar tile rounded="0" size="x-large" v-if="selected.ThumbnailUrl">
295
+ <img width="60" :src="selected.ThumbnailUrl">
296
+ </v-list-item-avatar>
297
+ </v-col>
298
+ <v-col cols="7">
299
+ <v-list-item-content>
300
+ <v-list-item-title>{{selected.title}}</v-list-item-title>
301
+ <v-list-item-subtitle>{{selected.subTitle}}</v-list-item-subtitle>
302
+ <v-list-item-subtitle v-if="playingTrack">{{playingTrack.Name}}</v-list-item-subtitle>
303
+ </v-list-item-content>
304
+ </v-col>
305
+ <v-col cols="4">
306
+ <v-select v-model="currentPlayer"
307
+ :items="playerNames"
308
+ persistent-hint
309
+ @update:modelValue="onPlayerChange"
310
+ dense solo hide-details single-line>
311
+ </v-select>
312
+ </v-col>
313
+ </v-row>
192
314
  </v-list-item>
193
315
  <v-card-actions>
194
316
  <v-btn v-if="backVisible" @click="listBack">
@@ -198,20 +320,27 @@
198
320
  Album
199
321
  <v-icon>{{$vuetify.icons.values.download}}</v-icon>
200
322
  </v-btn>
201
- <v-btn small v-if="playingTrackUrl" @click="playpause">
323
+ <v-btn v-if="!isLocal" @click="playFolder">
324
+ {{selected.title}}
325
+ <v-icon>{{$vuetify.icons.values.play}}</v-icon>
326
+ </v-btn>
327
+ <v-btn small v-if="playingTrack" @click="playpause">
202
328
  <v-icon>{{paused ? $vuetify.icons.values.play : $vuetify.icons.values.pause }}</v-icon>
203
329
  </v-btn>
204
- <v-btn small v-if="playingTrackUrl" @click="previous">
330
+ <v-btn small v-if="playingTrack" @click="previous">
205
331
  <v-icon>{{$vuetify.icons.values.prev}}</v-icon>
206
332
  </v-btn>
207
- <v-btn small v-if="playingTrackUrl" @click="next">
333
+ <v-btn small v-if="playingTrack" @click="next">
208
334
  <v-icon>{{$vuetify.icons.values.next}}</v-icon>
209
335
  </v-btn>
210
336
  <v-btn small v-if="downloadVisible" @click="download">
211
337
  <v-icon>{{$vuetify.icons.values.download}}</v-icon>
212
338
  </v-btn>
339
+ <v-slider v-if="playingTrack" style="width:30%" color="red" track-color="blue" track-fill-color="red" v-model="volume" hide-details density="compact"
340
+ min="0" max="100" step="5" @update:modelValue="changeVolume">
341
+ </v-slider>
213
342
  </v-card-actions>
214
- <v-progress-linear v-if="playingTrackUrl" v-model="positionLength" color="blue" height="25"><strong>{{positionText}}</strong></v-progress-linear>
343
+ <v-progress-linear v-if="playingTrack" v-model="positionLength" color="blue" height="25"><strong>{{positionText}}</strong></v-progress-linear>
215
344
  </v-card>
216
345
  <v-card :height="listHeight" class="overflow-y-auto bg-media">
217
346
  <v-list two-line class="bg-media">
@@ -160,7 +160,7 @@
160
160
  <v-icon large>{{$vuetify.icons.values.back}}</v-icon>Back
161
161
  </v-btn>
162
162
  <v-rating clearable length="2" v-model="selected.rating" @update:modelValue="onRating" />
163
- <v-select v-model="selected.root" c
163
+ <v-select v-model="selected.root"
164
164
  :items="roots"
165
165
  persistent-hint
166
166
  @update:modelValue="onRootChange"