ngx-media 1.0.0

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.
Files changed (101) hide show
  1. package/.agents/skills/angular-developer/SKILL.md +131 -0
  2. package/.agents/skills/angular-developer/references/angular-animations.md +160 -0
  3. package/.agents/skills/angular-developer/references/angular-aria.md +597 -0
  4. package/.agents/skills/angular-developer/references/cli.md +86 -0
  5. package/.agents/skills/angular-developer/references/component-harnesses.md +57 -0
  6. package/.agents/skills/angular-developer/references/component-styling.md +91 -0
  7. package/.agents/skills/angular-developer/references/components.md +117 -0
  8. package/.agents/skills/angular-developer/references/creating-services.md +97 -0
  9. package/.agents/skills/angular-developer/references/data-resolvers.md +69 -0
  10. package/.agents/skills/angular-developer/references/define-routes.md +67 -0
  11. package/.agents/skills/angular-developer/references/defining-providers.md +72 -0
  12. package/.agents/skills/angular-developer/references/di-fundamentals.md +116 -0
  13. package/.agents/skills/angular-developer/references/e2e-testing.md +39 -0
  14. package/.agents/skills/angular-developer/references/effects.md +83 -0
  15. package/.agents/skills/angular-developer/references/environment-configuration.md +132 -0
  16. package/.agents/skills/angular-developer/references/hierarchical-injectors.md +43 -0
  17. package/.agents/skills/angular-developer/references/host-elements.md +80 -0
  18. package/.agents/skills/angular-developer/references/injection-context.md +63 -0
  19. package/.agents/skills/angular-developer/references/inputs.md +101 -0
  20. package/.agents/skills/angular-developer/references/linked-signal.md +59 -0
  21. package/.agents/skills/angular-developer/references/loading-strategies.md +61 -0
  22. package/.agents/skills/angular-developer/references/mcp.md +106 -0
  23. package/.agents/skills/angular-developer/references/migrations.md +30 -0
  24. package/.agents/skills/angular-developer/references/navigate-to-routes.md +69 -0
  25. package/.agents/skills/angular-developer/references/outputs.md +86 -0
  26. package/.agents/skills/angular-developer/references/reactive-forms.md +118 -0
  27. package/.agents/skills/angular-developer/references/rendering-strategies.md +44 -0
  28. package/.agents/skills/angular-developer/references/resource.md +74 -0
  29. package/.agents/skills/angular-developer/references/route-animations.md +56 -0
  30. package/.agents/skills/angular-developer/references/route-guards.md +52 -0
  31. package/.agents/skills/angular-developer/references/router-lifecycle.md +45 -0
  32. package/.agents/skills/angular-developer/references/router-testing.md +87 -0
  33. package/.agents/skills/angular-developer/references/show-routes-with-outlets.md +68 -0
  34. package/.agents/skills/angular-developer/references/signal-forms.md +905 -0
  35. package/.agents/skills/angular-developer/references/signals-overview.md +94 -0
  36. package/.agents/skills/angular-developer/references/tailwind-css.md +69 -0
  37. package/.agents/skills/angular-developer/references/template-driven-forms.md +114 -0
  38. package/.agents/skills/angular-developer/references/testing-fundamentals.md +63 -0
  39. package/.editorconfig +17 -0
  40. package/.github/workflows/release.yml +39 -0
  41. package/.husky/commit-msg +1 -0
  42. package/.husky/pre-commit +1 -0
  43. package/.prettierignore +6 -0
  44. package/.prettierrc +17 -0
  45. package/.releaserc.json +41 -0
  46. package/.vscode/extensions.json +4 -0
  47. package/.vscode/launch.json +20 -0
  48. package/.vscode/tasks.json +42 -0
  49. package/CHANGELOG.md +132 -0
  50. package/CONTRIBUTING.md +70 -0
  51. package/README.md +19 -0
  52. package/angular.json +118 -0
  53. package/commitlint.config.js +3 -0
  54. package/package.json +52 -0
  55. package/projects/demo/.postcssrc.json +5 -0
  56. package/projects/demo/public/favicon.ico +0 -0
  57. package/projects/demo/src/app/app.config.server.ts +10 -0
  58. package/projects/demo/src/app/app.config.ts +9 -0
  59. package/projects/demo/src/app/app.css +5 -0
  60. package/projects/demo/src/app/app.html +1 -0
  61. package/projects/demo/src/app/app.routes.server.ts +8 -0
  62. package/projects/demo/src/app/app.routes.ts +28 -0
  63. package/projects/demo/src/app/app.spec.ts +23 -0
  64. package/projects/demo/src/app/app.ts +12 -0
  65. package/projects/demo/src/app/components/about/about.css +0 -0
  66. package/projects/demo/src/app/components/about/about.html +1 -0
  67. package/projects/demo/src/app/components/about/about.ts +9 -0
  68. package/projects/demo/src/app/components/home/home.css +5 -0
  69. package/projects/demo/src/app/components/home/home.html +8 -0
  70. package/projects/demo/src/app/components/home/home.ts +11 -0
  71. package/projects/demo/src/app/components/layout/layout.css +13 -0
  72. package/projects/demo/src/app/components/layout/layout.html +10 -0
  73. package/projects/demo/src/app/components/layout/layout.ts +10 -0
  74. package/projects/demo/src/app/components/samples/samples.css +0 -0
  75. package/projects/demo/src/app/components/samples/samples.html +1 -0
  76. package/projects/demo/src/app/components/samples/samples.ts +9 -0
  77. package/projects/demo/src/app/components/video-horizontal/video-horizontal.css +0 -0
  78. package/projects/demo/src/app/components/video-horizontal/video-horizontal.html +3 -0
  79. package/projects/demo/src/app/components/video-horizontal/video-horizontal.ts +20 -0
  80. package/projects/demo/src/app/components/video-vertical/video-vertical.css +0 -0
  81. package/projects/demo/src/app/components/video-vertical/video-vertical.html +3 -0
  82. package/projects/demo/src/app/components/video-vertical/video-vertical.ts +25 -0
  83. package/projects/demo/src/index.html +13 -0
  84. package/projects/demo/src/main.server.ts +7 -0
  85. package/projects/demo/src/main.ts +5 -0
  86. package/projects/demo/src/server.ts +66 -0
  87. package/projects/demo/src/styles.css +3 -0
  88. package/projects/demo/tsconfig.app.json +10 -0
  89. package/projects/demo/tsconfig.spec.json +10 -0
  90. package/projects/ngx-media/README.md +172 -0
  91. package/projects/ngx-media/ng-package.json +7 -0
  92. package/projects/ngx-media/package.json +31 -0
  93. package/projects/ngx-media/src/lib/models/camera.types.ts +14 -0
  94. package/projects/ngx-media/src/lib/ngx-media.ts +257 -0
  95. package/projects/ngx-media/src/lib/utils/media-capture-error.ts +14 -0
  96. package/projects/ngx-media/src/public-api.ts +7 -0
  97. package/projects/ngx-media/tsconfig.lib.json +13 -0
  98. package/projects/ngx-media/tsconfig.lib.prod.json +11 -0
  99. package/projects/ngx-media/tsconfig.spec.json +10 -0
  100. package/skills-lock.json +11 -0
  101. package/tsconfig.json +40 -0
@@ -0,0 +1,10 @@
1
+ /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
2
+ /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
3
+ {
4
+ "extends": "../../tsconfig.json",
5
+ "compilerOptions": {
6
+ "outDir": "../../out-tsc/spec",
7
+ "types": ["vitest/globals"]
8
+ },
9
+ "include": ["src/**/*.d.ts", "src/**/*.spec.ts"]
10
+ }
@@ -0,0 +1,172 @@
1
+ # What it is
2
+
3
+ _Angular library for easier camera/media access in web apps._
4
+
5
+ ---
6
+
7
+ # Planned features
8
+
9
+ - Auto Destroy and clearance on component destroy
10
+ - Easy to use ML features
11
+ - Easy to use component
12
+
13
+ ---
14
+
15
+ # Installation
16
+
17
+ ```bash
18
+ npm install ngx-media
19
+ ```
20
+
21
+ ---
22
+
23
+ # Basic usage
24
+
25
+ A simple usage is like this:
26
+
27
+ ```ts
28
+ @Component({
29
+ selector: 'app-video',
30
+ imports: [AsyncPipe],
31
+ template: ` <video #video width="300" height="400" autoplay [srcObject]="stream$ | async"></video> `,
32
+ })
33
+ export class Video {
34
+ private readonly destroyRef = inject(DestroyRef);
35
+ private readonly ngxMedia = new NgxMedia(this.destroyRef);
36
+
37
+ protected readonly stream$ = this.ngxMedia.stream$;
38
+
39
+ constructor() {
40
+ this.ngxMedia.startStream({
41
+ video: { facingMode: 'user', aspectRatio: 1, width: { exact: 400 }, height: { exact: 300 }, frameRate: 30 },
42
+ });
43
+ }
44
+ }
45
+ ```
46
+
47
+ - Notice the `DestroyRef` is being passed as the only argument the constructor. It automatically ends the stream when the component destroys.
48
+
49
+ Usage without passing `DestroyRef`:
50
+
51
+ ```ts
52
+ @Component({
53
+ selector: 'app-video',
54
+ template: ` <video #video width="300" height="400" autoplay [srcObject]="stream()"></video> `,
55
+ })
56
+ export class Video implements OnDestroy {
57
+ private readonly ngxMedia = new NgxMedia();
58
+
59
+ protected readonly stream = this.ngxMedia.stream;
60
+
61
+ constructor() {
62
+ this.ngxMedia.startStream({
63
+ video: { facingMode: 'user', aspectRatio: 1, width: { exact: 400 }, height: { exact: 300 }, frameRate: 30 },
64
+ });
65
+ }
66
+
67
+ ngOnDestroy() {
68
+ this.ngxMedia.destroy();
69
+ }
70
+ }
71
+ ```
72
+
73
+ - You must call `destroy` at some point or the device's camera keeps streaming the video or if you just call `stopStream` some internally observables and memory allocated tasks might live and affect the memory.
74
+
75
+ If you run `NgxMedia` outside of a injection context you need to pass the `Injector` to the constructor too so the signals can work fine.
76
+
77
+ ### **Avoid this pattern**:
78
+
79
+ ```ts
80
+ @Component({
81
+ ...
82
+ })
83
+ export class MyBadComponent {
84
+ private readonly destroyRef = inject(DestroyRef);
85
+ private readonly injector = inject(Injector);
86
+
87
+ someMethodOutsideOfInjectionContext() {
88
+ const ngxMedia = new NgxMedia(this.destroyRef, this.injector);
89
+ ngxMedia.startStream({
90
+ video: { facingMode: 'user', aspectRatio: 1, width: { exact: 400 }, height: { exact: 300 }, frameRate: 30 },
91
+ });
92
+ }
93
+ }
94
+ ```
95
+
96
+ ---
97
+
98
+ # APIs
99
+
100
+ ## Properties
101
+
102
+ ### `devices$: Observable<MediaDeviceInfo[]>`
103
+
104
+ Available video-input devices. Populated automatically when the `NgxMedia` instance is created by calling `navigator.mediaDevices.enumerateDevices()`.
105
+
106
+ ### `constraints: Observable<MediaStreamConstraints | null>`
107
+
108
+ Latest constraints used to request a stream. `null` when no stream has been requested yet.
109
+
110
+ ### `stream$: Observable<MediaStream | null>`
111
+
112
+ Currently active `MediaStream` object. `null` when no stream is active.
113
+
114
+ ### `status$: Observable<CameraStatus>`
115
+
116
+ Current lifecycle status of the instance.
117
+
118
+ | Status | Description |
119
+ | ------------ | ----------------------------------------------------- |
120
+ | `IDLE` | Instance created, no stream requested yet |
121
+ | `REQUESTING` | Waiting for the device to return a stream |
122
+ | `STREAMING` | Stream is active |
123
+ | `STOPPED` | Stream was stopped (at least one stream has occurred) |
124
+ | `ERROR` | An error occurred during one of the processes |
125
+
126
+ ### `error$: Observable<MediaCaptureError | null>`
127
+
128
+ Latest caught error. `null` when no error has occurred.
129
+
130
+ `MediaCaptureError` codes: `permission-denied`, `device-not-found`, `https-required`, `device-busy`, `unsupported`, `unknown`.
131
+
132
+ ---
133
+
134
+ ## Methods
135
+
136
+ ### `getDevices$(): Observable<MediaDeviceInfo[]>`
137
+
138
+ Calls `navigator.mediaDevices.enumerateDevices()` and returns an observable of video-input devices. Also updates `devices$`.
139
+
140
+ Throws `MediaCaptureError` when the MediaDevices API is unavailable or HTTPS is required.
141
+
142
+ ### `getDevices(): void`
143
+
144
+ Triggers `getDevices$()` and updates `devices$` internally. Fire-and-forget convenience wrapper.
145
+
146
+ ### `startStream$(constraints: MediaStreamConstraints): Observable<MediaStream>`
147
+
148
+ Requests a new stream with the given constraints. If already streaming, returns the current stream. To change constraints on an active stream use `switchConstraints()` instead.
149
+
150
+ ### `startStream(constraints: MediaStreamConstraints, onStart?: (stream: MediaStream) => void): void`
151
+
152
+ Fire-and-forget wrapper around `startStream$()`. Optional `onStart` callback is invoked with the stream once active.
153
+
154
+ ### `stopStream$(): Observable<MediaStream>`
155
+
156
+ Stops all tracks on the current stream, sets `stream$` to `null`, and updates status to `STOPPED`.
157
+
158
+ ### `stopStream(onStop?: (stream: MediaStream) => void): void`
159
+
160
+ Fire-and-forget wrapper around `stopStream$()`. Optional `onStop` callback is invoked with the stream before cleanup.
161
+
162
+ ### `switchConstraints(newConstraints: MediaStreamConstraints): void`
163
+
164
+ Stops the current stream and starts a new one with the provided constraints.
165
+
166
+ ---
167
+
168
+ # Browser notes
169
+
170
+ - HTTPS required
171
+ - Permission handling
172
+ - Mobile Safari caveats
@@ -0,0 +1,7 @@
1
+ {
2
+ "$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
3
+ "dest": "../../dist/ngx-media",
4
+ "lib": {
5
+ "entryFile": "src/public-api.ts"
6
+ }
7
+ }
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "ngx-media",
3
+ "version": "1.0.0",
4
+ "description": "Angular library for easier camera/media access in web apps",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/mybloodyusername/ngx-media.git"
8
+ },
9
+ "bugs": {
10
+ "url": "https://github.com/mybloodyusername/ngx-media/issues"
11
+ },
12
+ "homepage": "https://github.com/mybloodyusername/ngx-media#readme",
13
+ "keywords": [
14
+ "angular",
15
+ "media",
16
+ "camera",
17
+ "video",
18
+ "webrtc"
19
+ ],
20
+ "peerDependencies": {
21
+ "@angular/common": "^22.0.0",
22
+ "@angular/core": "^22.0.0"
23
+ },
24
+ "dependencies": {
25
+ "tslib": "^2.3.0"
26
+ },
27
+ "sideEffects": false,
28
+ "publishConfig": {
29
+ "access": "public"
30
+ }
31
+ }
@@ -0,0 +1,14 @@
1
+ export enum CameraStatus {
2
+ IDLE = 'IDLE',
3
+ REQUESTING = 'REQUESTING',
4
+ STREAMING = 'STREAMING',
5
+ STOPPED = 'STOPPED',
6
+ ERROR = 'ERROR',
7
+ }
8
+
9
+ export interface StartCameraOptions {
10
+ audio?: false;
11
+ video?: MediaTrackConstraints | boolean;
12
+ deviceId?: string;
13
+ facingMode?: 'user' | 'environment';
14
+ }
@@ -0,0 +1,257 @@
1
+ import { BehaviorSubject, catchError, defer, filter, from, map, Observable, take, tap, throwError } from 'rxjs';
2
+ import { CameraStatus } from './models/camera.types';
3
+ import { MediaCaptureError } from './utils/media-capture-error';
4
+ import { toSignal } from '@angular/core/rxjs-interop';
5
+ import { DestroyRef, Injector, Signal } from '@angular/core';
6
+
7
+ export class NgxMedia {
8
+ private readonly _devices$ = new BehaviorSubject<MediaDeviceInfo[]>([]);
9
+ private readonly _constraints$ = new BehaviorSubject<MediaStreamConstraints | null>(null);
10
+ private readonly _stream$ = new BehaviorSubject<MediaStream | null>(null);
11
+ private readonly _status$ = new BehaviorSubject<CameraStatus>(CameraStatus.IDLE);
12
+ private readonly _error$ = new BehaviorSubject<MediaCaptureError | null>(null);
13
+
14
+ /**
15
+ * Available video-input devices, populated automatically on construction
16
+ * via `navigator.mediaDevices.enumerateDevices()`.
17
+ *
18
+ * Re-emits whenever {@link getDevices} or {@link getDevices$} is called.
19
+ */
20
+ get devices$(): Observable<MediaDeviceInfo[]> {
21
+ return this._devices$.asObservable();
22
+ }
23
+
24
+ /**
25
+ * Latest constraints used to request a stream.
26
+ * Emits `null` until the first call to {@link startStream$} or {@link startStream}.
27
+ */
28
+ get constraints$(): Observable<MediaStreamConstraints | null> {
29
+ return this._constraints$.asObservable();
30
+ }
31
+
32
+ /**
33
+ * Currently active `MediaStream`. Emits `null` when no stream is active.
34
+ */
35
+ get stream$(): Observable<MediaStream | null> {
36
+ return this._stream$.asObservable();
37
+ }
38
+
39
+ /**
40
+ * Current lifecycle status of this instance.
41
+ *
42
+ * - `IDLE` — Created, no stream requested yet.
43
+ * - `REQUESTING` — Waiting for the device to return a stream.
44
+ * - `STREAMING` — A stream is active.
45
+ * - `STOPPED` — Stream was explicitly stopped (at least one stream occurred previously).
46
+ * - `ERROR` — An error occurred during one of the processes.
47
+ *
48
+ * @see {@link CameraStatus}
49
+ */
50
+ get status$(): Observable<CameraStatus> {
51
+ return this._status$.asObservable();
52
+ }
53
+
54
+ /**
55
+ * Latest caught error. Emits `null` when no error has occurred.
56
+ *
57
+ * @see {@link MediaCaptureError}
58
+ */
59
+ get error$(): Observable<MediaCaptureError | null> {
60
+ return this._error$.asObservable();
61
+ }
62
+
63
+ /**
64
+ * devices$ as signal.
65
+ * @see devices$ */
66
+ readonly devices: Signal<MediaDeviceInfo[]>;
67
+
68
+ /**
69
+ * constraints$ as signal.
70
+ * @see constraints$ */
71
+ readonly constraints: Signal<MediaStreamConstraints | null>;
72
+
73
+ /**
74
+ * stream$ as signal.
75
+ * @see stream$ */
76
+ readonly stream: Signal<MediaStream | null>;
77
+
78
+ /**
79
+ * status$ as signal.
80
+ * @see status$ */
81
+ readonly status: Signal<CameraStatus>;
82
+
83
+ /**
84
+ * error$ as signal.
85
+ * @see error$ */
86
+ readonly error: Signal<MediaCaptureError | null>;
87
+
88
+ constructor(
89
+ private readonly destroyRef?: DestroyRef,
90
+ private readonly injector?: Injector,
91
+ ) {
92
+ this.getDevices();
93
+
94
+ this.devices = toSignal(this._devices$, { requireSync: true, injector });
95
+ this.constraints = toSignal(this._constraints$, { requireSync: true, injector });
96
+ this.stream = toSignal(this._stream$, { requireSync: true, injector });
97
+ this.status = toSignal(this._status$, { requireSync: true, injector });
98
+ this.error = toSignal(this._error$, { requireSync: true, injector });
99
+
100
+ destroyRef?.onDestroy(() => {
101
+ this.destroy();
102
+ });
103
+ }
104
+
105
+ /**
106
+ * Enumerates available video-input devices and updates {@link devices$}.
107
+ *
108
+ * @returns Observable that emits an array of `MediaDeviceInfo` for video-input devices.
109
+ * @throws {MediaCaptureError} `device-not-found` — MediaDevices API is not supported.
110
+ * @throws {MediaCaptureError} `https-required` — Page is not in a secure context.
111
+ *
112
+ * @example
113
+ * ```ts
114
+ * ngxMedia.getDevices$().subscribe((devices) => {
115
+ * console.log('Found cameras:', devices.length);
116
+ * });
117
+ * ```
118
+ */
119
+ getDevices$() {
120
+ return defer(() => {
121
+ if (!navigator.mediaDevices?.enumerateDevices) {
122
+ return throwError(() => new MediaCaptureError('MediaDevices API is not supported.', 'device-not-found'));
123
+ }
124
+
125
+ if (!window.isSecureContext) {
126
+ return throwError(
127
+ () => new MediaCaptureError('MediaDevices API requires HTTPS or localhost.', 'https-required'),
128
+ );
129
+ }
130
+
131
+ return from(navigator.mediaDevices.enumerateDevices()).pipe(
132
+ catchError((err) => {
133
+ this._status$.next(CameraStatus.ERROR);
134
+ return throwError(() => new MediaCaptureError(`Something went wrong: ${err?.message}`, 'unknown', err));
135
+ }),
136
+ map((mediaDeviceInfo) => mediaDeviceInfo.filter((m) => m.kind === 'videoinput')),
137
+ tap((v) => this._devices$.next(v)),
138
+ );
139
+ });
140
+ }
141
+
142
+ /**
143
+ * Fire-and-forget wrapper around {@link getDevices$}.
144
+ * Refreshes the {@link devices$} observable internally.
145
+ */
146
+ getDevices() {
147
+ this.getDevices$().subscribe();
148
+ }
149
+
150
+ /**
151
+ * Requests a new stream with the given constraints.
152
+ * If already streaming, returns the current stream — use {@link switchConstraints} to change
153
+ * constraints on an active stream.
154
+ *
155
+ * @param constraints - `MediaStreamConstraints` passed to `getUserMedia()`.
156
+ * @returns Observable that emits the active `MediaStream`.
157
+ * @throws {MediaCaptureError} When the device denies permission or is unavailable.
158
+ *
159
+ * @example
160
+ * ```ts
161
+ * ngxMedia.startStream$({ video: { facingMode: 'user' } }).subscribe((stream) => {
162
+ * videoElement.srcObject = stream;
163
+ * });
164
+ * ```
165
+ */
166
+ startStream$(constraints: MediaStreamConstraints) {
167
+ this._constraints$.next(constraints);
168
+ if (this._status$.value === CameraStatus.STREAMING) {
169
+ return this._stream$.asObservable() as Observable<MediaStream>;
170
+ }
171
+ this._status$.next(CameraStatus.REQUESTING);
172
+ return from(navigator.mediaDevices.getUserMedia(constraints)).pipe(
173
+ catchError((err) => {
174
+ this._status$.next(CameraStatus.ERROR);
175
+ return throwError(() => new MediaCaptureError(`Something went wrong: ${err?.message}`, 'unknown', err));
176
+ }),
177
+ tap((stream) => {
178
+ this._status$.next(CameraStatus.STREAMING);
179
+ this._stream$.next(stream);
180
+ }),
181
+ );
182
+ }
183
+
184
+ /**
185
+ * Fire-and-forget wrapper around {@link startStream$}.
186
+ *
187
+ * @param constraints - `MediaStreamConstraints` passed to `getUserMedia()`.
188
+ * @param onStart - Optional callback invoked with the `MediaStream` once active.
189
+ *
190
+ * @example
191
+ * ```ts
192
+ * ngxMedia.startStream(
193
+ * { video: true },
194
+ * (stream) => { videoElement.srcObject = stream; },
195
+ * );
196
+ * ```
197
+ */
198
+ startStream(constraints: MediaStreamConstraints, onStart?: (stream: MediaStream) => void) {
199
+ this.startStream$(constraints).subscribe((stream) => onStart && onStart(stream));
200
+ }
201
+
202
+ /**
203
+ * Stops all tracks on the current stream, sets {@link stream$} to `null`,
204
+ * and transitions {@link status$} to `STOPPED`.
205
+ *
206
+ * @returns Observable that emits the `MediaStream` that was stopped.
207
+ */
208
+ stopStream$() {
209
+ return this._stream$.pipe(
210
+ filter((s) => s instanceof MediaStream),
211
+ take(1),
212
+ tap((stream) => {
213
+ stream.getTracks().forEach((track) => {
214
+ track.stop();
215
+ });
216
+ this._stream$.next(null);
217
+ this._status$.next(CameraStatus.STOPPED);
218
+ }),
219
+ );
220
+ }
221
+
222
+ /**
223
+ * Fire-and-forget wrapper around {@link stopStream$}.
224
+ *
225
+ * @param onStop - Optional callback invoked with the `MediaStream` before cleanup.
226
+ */
227
+ stopStream(onStop?: (stream: MediaStream) => void) {
228
+ this.stopStream$().subscribe((stream) => onStop && onStop(stream));
229
+ }
230
+
231
+ /**
232
+ * Stops the current stream and immediately starts a new one with the provided constraints.
233
+ * Equivalent to calling {@link stopStream} followed by {@link startStream}.
234
+ *
235
+ * @param newConstraints - `MediaStreamConstraints` for the new stream.
236
+ */
237
+ switchConstraints(newConstraints: MediaStreamConstraints) {
238
+ this.stopStream();
239
+ this.startStream(newConstraints);
240
+ }
241
+
242
+ destroy() {
243
+ const stream = this._stream$.value;
244
+ if (stream) {
245
+ stream.getTracks().forEach((track) => track.stop());
246
+ this._stream$.next(null);
247
+ }
248
+
249
+ this._status$.next(CameraStatus.STOPPED);
250
+
251
+ this._devices$.complete();
252
+ this._constraints$.complete();
253
+ this._stream$.complete();
254
+ this._status$.complete();
255
+ this._error$.complete();
256
+ }
257
+ }
@@ -0,0 +1,14 @@
1
+ export class MediaCaptureError extends Error {
2
+ readonly code:
3
+ 'permission-denied' | 'device-not-found' | 'https-required' | 'device-busy' | 'unsupported' | 'unknown';
4
+ readonly originalError: unknown;
5
+ constructor(
6
+ message: string,
7
+ code: 'permission-denied' | 'device-not-found' | 'https-required' | 'device-busy' | 'unsupported' | 'unknown',
8
+ originalError?: unknown,
9
+ ) {
10
+ super(message);
11
+ this.code = code;
12
+ this.originalError = originalError;
13
+ }
14
+ }
@@ -0,0 +1,7 @@
1
+ /*
2
+ * Public API Surface of ngx-media
3
+ */
4
+ export * from './lib/ngx-media';
5
+
6
+ export * from './lib/models/camera.types';
7
+ export * from './lib/utils/media-capture-error';
@@ -0,0 +1,13 @@
1
+ /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
2
+ /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
3
+ {
4
+ "extends": "../../tsconfig.json",
5
+ "compilerOptions": {
6
+ "outDir": "../../out-tsc/lib",
7
+ "declaration": true,
8
+ "declarationMap": true,
9
+ "types": []
10
+ },
11
+ "include": ["src/**/*.ts"],
12
+ "exclude": ["**/*.spec.ts"]
13
+ }
@@ -0,0 +1,11 @@
1
+ /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
2
+ /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
3
+ {
4
+ "extends": "./tsconfig.lib.json",
5
+ "compilerOptions": {
6
+ "declarationMap": false
7
+ },
8
+ "angularCompilerOptions": {
9
+ "compilationMode": "partial"
10
+ }
11
+ }
@@ -0,0 +1,10 @@
1
+ /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
2
+ /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
3
+ {
4
+ "extends": "../../tsconfig.json",
5
+ "compilerOptions": {
6
+ "outDir": "../../out-tsc/spec",
7
+ "types": ["vitest/globals"]
8
+ },
9
+ "include": ["src/**/*.d.ts", "src/**/*.spec.ts"]
10
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "version": 1,
3
+ "skills": {
4
+ "angular-developer": {
5
+ "source": "angular/skills",
6
+ "sourceType": "github",
7
+ "skillPath": "angular-developer/SKILL.md",
8
+ "computedHash": "400d674a14120ccaa435a263cd6551e5f5ddb84d99df8622689da46fe9e5902c"
9
+ }
10
+ }
11
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,40 @@
1
+ /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
2
+ /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
3
+ {
4
+ "compileOnSave": false,
5
+ "compilerOptions": {
6
+ "noImplicitOverride": true,
7
+ "noPropertyAccessFromIndexSignature": true,
8
+ "noImplicitReturns": true,
9
+ "noFallthroughCasesInSwitch": true,
10
+ "paths": {
11
+ "ngx-media": ["./dist/ngx-media"]
12
+ },
13
+ "skipLibCheck": true,
14
+ "isolatedModules": true,
15
+ "experimentalDecorators": true,
16
+ "importHelpers": true,
17
+ "target": "ES2022",
18
+ "module": "preserve"
19
+ },
20
+ "angularCompilerOptions": {
21
+ "enableI18nLegacyMessageIdFormat": false,
22
+ "strictInjectionParameters": true,
23
+ "strictInputAccessModifiers": true
24
+ },
25
+ "files": [],
26
+ "references": [
27
+ {
28
+ "path": "./projects/ngx-media/tsconfig.lib.json"
29
+ },
30
+ {
31
+ "path": "./projects/ngx-media/tsconfig.spec.json"
32
+ },
33
+ {
34
+ "path": "./projects/demo/tsconfig.app.json"
35
+ },
36
+ {
37
+ "path": "./projects/demo/tsconfig.spec.json"
38
+ }
39
+ ]
40
+ }