Skip to content

Commit

Permalink
Initial implementation of media player
Browse files Browse the repository at this point in the history
  • Loading branch information
sleepyfran committed Aug 22, 2024
1 parent 2830809 commit 8e60d11
Show file tree
Hide file tree
Showing 36 changed files with 665 additions and 102 deletions.
3 changes: 2 additions & 1 deletion packages/components/add-provider/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"dependencies": {
"@echo/core-types": "^1.0.0",
"@echo/services-bootstrap": "^1.0.0",
"@echo/services-bootstrap-services": "^1.0.0",
"@effect-rx/rx": "^0.33.8",
"@effect-rx/rx-react": "^0.30.11",
"effect": "^3.2.8"
Expand All @@ -17,4 +18,4 @@
"@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22"
}
}
}
55 changes: 50 additions & 5 deletions packages/components/add-provider/src/AddProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,36 @@
import {
ActiveMediaProviderCache,
AvailableProviders,
MediaProviderMainThreadBroadcastChannel,
type Authentication,
type AuthenticationInfo,
type FolderMetadata,
type MediaPlayer,
type MediaProvider,
type ProviderMetadata,
} from "@echo/core-types";
import { LazyLoadedProvider, MainLive } from "@echo/services-bootstrap";
import { LazyLoadedProvider } from "@echo/services-bootstrap";
import { AppLive } from "@echo/services-bootstrap-services";
import { LazyLoadedMediaPlayer } from "@echo/services-bootstrap/src/loaders";
import { Rx } from "@effect-rx/rx";
import { useRx } from "@effect-rx/rx-react";
import { Effect, Match } from "effect";
import { useCallback, useEffect, useMemo } from "react";

const runtime = Rx.runtime(MainLive);
const loadProviderFn = runtime.fn((metadata: ProviderMetadata) =>
LazyLoadedProvider.load(metadata),
const runtime = Rx.runtime(AppLive);
const loadProviderFn = runtime.fn(LazyLoadedProvider.load);
const loadMediaPlayerFn = runtime.fn(
({
metadata,
authInfo,
}: {
metadata: ProviderMetadata;
authInfo: AuthenticationInfo;
}) =>
Effect.gen(function* () {
const { createMediaPlayer } = yield* LazyLoadedMediaPlayer.load(metadata);
return yield* createMediaPlayer(authInfo);
}),
);

export const AddProvider = () => {
Expand Down Expand Up @@ -105,6 +120,18 @@ const listRootFn = runtime.fn(
(mediaProvider: MediaProvider) => mediaProvider.listRoot,
);

const addMediaProviderToCacheFn = runtime.fn(
({
metadata,
provider,
player,
}: {
metadata: ProviderMetadata;
provider: MediaProvider;
player: MediaPlayer;
}) => ActiveMediaProviderCache.add(metadata, provider, player),
);

const SelectRoot = ({
authInfo,
metadata,
Expand All @@ -114,16 +141,34 @@ const SelectRoot = ({
metadata: ProviderMetadata;
createMediaProvider: (authInfo: AuthenticationInfo) => MediaProvider;
}) => {
const [mediaPlayerStatus, createMediaPlayer] = useRx(loadMediaPlayerFn);

const mediaProvider = useMemo(
() => createMediaProvider(authInfo),
[createMediaProvider, authInfo],
);

const [listStatus, listRoot] = useRx(listRootFn);
const [, addMediaProviderToCache] = useRx(addMediaProviderToCacheFn);

useEffect(() => {
listRoot(mediaProvider);
});
createMediaPlayer({ metadata, authInfo });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

// TODO: Refactor this mess somehow?
useEffect(() => {
Match.value(mediaPlayerStatus).pipe(
Match.tag("Success", ({ value: player }) => {
addMediaProviderToCache({
metadata,
provider: mediaProvider,
player,
});
}),
);
}, [mediaPlayerStatus, mediaProvider, metadata, addMediaProviderToCache]);

return (
<div>
Expand Down
3 changes: 2 additions & 1 deletion packages/components/library/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@
},
"dependencies": {
"@echo/core-types": "^1.0.0",
"@echo/services-bootstrap": "^1.0.0",
"@echo/services-bootstrap-services": "^1.0.0",
"@echo/services-library": "^1.0.0",
"@echo/services-player": "^1.0.0",
"@effect-rx/rx": "^0.33.8",
"@effect-rx/rx-react": "^0.30.11",
"effect": "^3.2.8"
Expand Down
30 changes: 11 additions & 19 deletions packages/components/library/src/Library.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,30 @@
import { Library, type Track } from "@echo/core-types";
import { MainLive } from "@echo/services-bootstrap";
import { Library, Player } from "@echo/core-types";
import { AppLive } from "@echo/services-bootstrap-services";
import { Rx } from "@effect-rx/rx";
import { Layer, Stream } from "effect";
import { Suspense, useState } from "react";
import { Suspense } from "react";
import { LibraryLive } from "@echo/services-library";
import { useRxSuspenseSuccess } from "@effect-rx/rx-react";
import { PlayerLive } from "@echo/services-player";
import { useRx, useRxSuspenseSuccess } from "@effect-rx/rx-react";

const runtime = Rx.runtime(LibraryLive.pipe(Layer.provide(MainLive)));
const runtime = Rx.runtime(
Layer.mergeAll(LibraryLive, PlayerLive).pipe(Layer.provide(AppLive)),
);
const observeLibrary = runtime.rx(Stream.unwrap(Library.observeAlbums()));
const playAlbumFn = runtime.fn(Player.playAlbum);

const UserLibrary = () => {
const [src, setSrc] = useState<string | undefined>(undefined);
const albums = useRxSuspenseSuccess(observeLibrary).value;

const playFirstTrack = (tracks: Track[]) => {
const track = tracks[0];
switch (track.resource.type) {
case "file":
setSrc(track.resource.uri);
break;
case "api":
break;
}
};
const [, playAlbum] = useRx(playAlbumFn);

return (
<div>
<br />
<audio src={src} autoPlay controls />
{albums.map((album) => (
<div key={album.id}>
<h3>{album.name}</h3>
<p>{album.artist.name}</p>
<button onClick={() => playFirstTrack(album.tracks)}>Play</button>
<button onClick={() => playAlbum(album)}>Play</button>
<hr />
</div>
))}
Expand Down
3 changes: 1 addition & 2 deletions packages/components/provider-status/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@
},
"dependencies": {
"@echo/core-types": "^1.0.0",
"@echo/services-bootstrap": "^1.0.0",
"@echo/services-media-provider-status": "^1.0.0",
"@echo/services-bootstrap-services": "^1.0.0",
"@effect-rx/rx": "^0.33.8",
"@effect-rx/rx-react": "^0.30.11",
"effect": "^3.2.8"
Expand Down
9 changes: 3 additions & 6 deletions packages/components/provider-status/src/ProviderStatus.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import { MediaProviderStatusLive } from "@echo/services-media-provider-status";
import { MainLive } from "@echo/services-bootstrap";
import { AppLive } from "@echo/services-bootstrap-services";
import { Rx } from "@effect-rx/rx";
import { Layer, Match } from "effect";
import { Match } from "effect";
import { useRxValue } from "@effect-rx/rx-react";
import { MediaProviderStatus } from "@echo/core-types";

const runtime = Rx.runtime(
MediaProviderStatusLive.pipe(Layer.provide(MainLive)),
);
const runtime = Rx.runtime(AppLive);

const providerStatus = runtime.subscriptionRef(MediaProviderStatus.observe);

Expand Down
18 changes: 16 additions & 2 deletions packages/core/types/src/model/file-system.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,23 @@
import { Brand } from "effect";

/**
* A unique identifier for a folder in a media provider's file system.
*/
export type FolderId = string & Brand.Brand<"FolderId">;
export const FolderId = Brand.nominal<FolderId>();

/**
* A unique identifier for a file in a media provider's file system.
*/
export type FileId = string & Brand.Brand<"FileId">;
export const FileId = Brand.nominal<FileId>();

/**
* Defines a folder in a media provider's file system.
*/
export type FolderMetadata = {
_tag: "folder";
id: string;
id: FolderId;
name: string;
};

Expand All @@ -12,7 +26,7 @@ export type FolderMetadata = {
*/
export type FileMetadata = {
_tag: "file";
id: string;
id: FileId;
name: string;
byteSize: number;
mimeType: string | undefined;
Expand Down
3 changes: 2 additions & 1 deletion packages/core/types/src/model/track.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {
ApiBasedProviderId,
FileBasedProviderId,
} from "./provider-metadata";
import type { FileId } from "./file-system";

/**
* Wrapper around a string to represent a track id.
Expand All @@ -17,7 +18,7 @@ export const TrackId = Brand.nominal<TrackId>();
*/
export type StreamingResource =
// TODO: Do not store URIs. They seem to expire after some time. Instead save the track ID and the provider ID.
| { type: "file"; provider: FileBasedProviderId; uri: string }
| { type: "file"; provider: FileBasedProviderId; fileId: FileId }
| { type: "api"; provider: ApiBasedProviderId };

/**
Expand Down
49 changes: 49 additions & 0 deletions packages/core/types/src/services/active-media-provider-cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Effect, Option } from "effect";
import type { ProviderId, ProviderMetadata } from "../model";
import type { MediaPlayer, MediaProvider } from "./media-provider";

type ProviderWithMetadata = {
readonly metadata: ProviderMetadata;
readonly provider: MediaProvider;
readonly player: MediaPlayer;
};

/**
* Defines a cache of all currently active providers.
*/
export type MediaProviderById = Map<ProviderId, ProviderWithMetadata>;

/**
* Service that allows to cache active media providers and observe changes to
* them.
*/
export type IActiveMediaProviderCache = {
/**
* Adds the given provider to the cache. If a provider with the same ID is
* already present, it will be replaced. Upon being added, the service will
* listen to the provider's state changes and remove it from the cache once
* it becomes inactive.
*/
readonly add: (
metadata: ProviderMetadata,
provider: MediaProvider,
player: MediaPlayer,
) => Effect.Effect<void>;

/**
* Returns a media provider, if it is currently active, or none otherwise.
*/
readonly get: (providerId: ProviderId) => Effect.Effect<
Option.Option<{
provider: MediaProvider;
player: MediaPlayer;
}>
>;
};

/**
* Tag to identify the ActiveMediaProviderCache service.
*/
export class ActiveMediaProviderCache extends Effect.Tag(
"@echo/core-types/ActiveMediaProviderCache",
)<ActiveMediaProviderCache, IActiveMediaProviderCache>() {}
4 changes: 3 additions & 1 deletion packages/core/types/src/services/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
export * from "./active-media-provider-cache";
export * from "./authentication";
export * from "./broadcast-channel";
export * from "./crypto";
export * from "./database";
export * from "./library";
export * from "./metadata-provider";
export * from "./mediaProvider";
export * from "./media-player";
export * from "./media-provider";
export * from "./player";
export * from "./provider-status";
52 changes: 52 additions & 0 deletions packages/core/types/src/services/media-player.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Effect, Stream } from "effect";

/**
* Current state in which the media player is in.
*/
export type MediaPlayerState =
| { _tag: "idle" }
| { _tag: "playing" }
| { _tag: "paused" };

/**
* Service that provides media playback capabilities and exposes the current
* state of the player.
*/
type IMediaPlayer<TStreamingSource> = {
/**
* Starts the player with the given source.
*/
readonly play: (fromSource: TStreamingSource) => Effect.Effect<void>;

/**
* Returns a stream that emits the current player state and any subsequent
* changes to it.
*/
readonly observe: Effect.Effect<Stream.Stream<MediaPlayerState>>;
};

/**
* A media player that can play tracks that are hosted on a file-system.
*/
export type IFileMediaPlayer = IMediaPlayer<URL>;

/**
* A media player that can play tracks that are hosted on a third-party API.
*/
export type IApiMediaPlayer = IMediaPlayer<never>;

/**
* Tag to identify a file-based media player.
*/
export class FileMediaPlayer extends Effect.Tag("@echo/core-types/Library")<
FileMediaPlayer,
IMediaPlayer<URL>
>() {}

/**
* Tag to identify an API-based media player.
*/
export class ApiMediaPlayer extends Effect.Tag("@echo/core-types/Library")<
ApiMediaPlayer,
IMediaPlayer<never>
>() {}
Loading

0 comments on commit 8e60d11

Please sign in to comment.