@ukrit.fb/live-publisher 0.0.0-stage → 0.1.0-alpha.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.
package/README.md CHANGED
@@ -1,3 +1,163 @@
1
- # Temporary Holding Version
1
+ # @ukrit.fb/live-publisher
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ library สำหรับหน้าเว็บร้านค้า ให้คนขาย live ขายของจาก browser ได้ทั้งคอมพิวเตอร์และมือถือ เปิดกล้องและไมโครโฟน แล้วส่งภาพขึ้นระบบ live ผ่าน WebRTC (WHIP) คนดูดูผ่านลิงก์ผู้ชม (HLS)
4
+
5
+ - เปิดกล้อง แสดงภาพตัวอย่าง เลือกและสลับกล้อง/ไมค์ กล้องหน้า/หลัง
6
+ - เริ่ม live, จบ live, ปิด/เปิดภาพและเสียงระหว่าง live
7
+ - รับมือเมื่อเสียบหรือถอดอุปกรณ์ และเมื่อ connection หลุด
8
+ - error ทุกตัวมี `code` ที่คงที่ ใช้เขียนเงื่อนไขและแปลข้อความเองได้
9
+ - ไม่เก็บและไม่ log stream key
10
+ - ใช้ได้ทั้ง npm (ESM + TypeScript types) และ CDN ไม่มี dependency
11
+
12
+ > **สถานะ: alpha (`0.1.0-alpha.x`) สำหรับทดสอบภายในทีม ยังไม่พร้อมใช้บนหน้าเว็บจริง**
13
+ >
14
+ > - flow ครบและ live จริงผ่านแล้วบน Chrome, Safari, Firefox บนคอมพิวเตอร์ ยังไม่ได้ทดสอบบนมือถือจริงและ Edge
15
+ > - ระหว่างที่ WHIP ของ server ยังเป็น `http://` ให้ทดสอบบนหน้า `http://localhost` และสร้าง publisher ด้วย `new Publisher({ allowInsecure: true })` บนหน้า HTTPS จะได้ `MIXED_CONTENT`
16
+ > - API อาจเปลี่ยนได้ก่อน 1.0 ดูการเปลี่ยนแปลงใน CHANGELOG ของ repo
17
+
18
+ ## ติดตั้ง
19
+
20
+ ```sh
21
+ npm install @ukrit.fb/live-publisher@alpha
22
+ ```
23
+
24
+ หรือผ่าน CDN (ได้ตัวแปร global ชื่อ `LivePublisher`)
25
+
26
+ ```html
27
+ <script src="https://unpkg.com/@ukrit.fb/live-publisher@alpha"></script>
28
+ <script>
29
+ const publisher = new LivePublisher.Publisher()
30
+ </script>
31
+ ```
32
+
33
+ > ต้องเปิดหน้าเว็บผ่าน **HTTPS** (หรือ `localhost` ตอนพัฒนา) ไม่อย่างนั้น browser ไม่ให้ใช้กล้อง
34
+
35
+ ## เริ่มต้นใช้งาน
36
+
37
+ ```html
38
+ <video id="preview"></video>
39
+ <button id="camera">เปิดกล้อง</button>
40
+ <button id="start">เริ่ม live</button>
41
+ <button id="stop">จบ live</button>
42
+ ```
43
+
44
+ ```ts
45
+ import { Publisher, checkSupport, isPublisherError } from '@ukrit.fb/live-publisher'
46
+
47
+ // 1. เช็กว่า browser นี้ live ได้ไหม (ไม่ throw และไม่ขอสิทธิ์กล้อง)
48
+ const support = await checkSupport()
49
+ if (!support.supported) {
50
+ // แสดงเหตุผลจาก support.issues แล้วไม่ต้องแสดงหน้า live
51
+ }
52
+
53
+ // 2. สร้างครั้งเดียวต่อหน้า
54
+ const publisher = new Publisher()
55
+
56
+ // 3. เปิดกล้อง ให้คนขายเห็นภาพตัวเองก่อน (browser ขอสิทธิ์ตรงนี้)
57
+ document.querySelector('#camera')!.addEventListener('click', async () => {
58
+ await publisher.startPreview({ video: document.querySelector('video')! })
59
+ })
60
+
61
+ // 4. เริ่ม live: library เรียก getCredentials ตอนจะเชื่อมต่อ ให้ backend ของร้านขอ WHIP URL
62
+ document.querySelector('#start')!.addEventListener('click', async () => {
63
+ try {
64
+ await publisher.start({
65
+ getCredentials: async () => (await fetch('/api/live/credentials', { method: 'POST' })).json(),
66
+ })
67
+ } catch (error) {
68
+ if (isPublisherError(error) && error.code !== 'ABORTED')
69
+ alert(`เริ่ม live ไม่สำเร็จ (${error.code})`)
70
+ }
71
+ })
72
+
73
+ // 5. จบ live และปิดกล้อง
74
+ document.querySelector('#stop')!.addEventListener('click', () => publisher.stop())
75
+
76
+ // 6. ออกจากหน้า: ปล่อยกล้องและ connection
77
+ window.addEventListener('pagehide', () => publisher.destroy())
78
+ ```
79
+
80
+ ต้องเรียก `startPreview()` ก่อน `start()` เสมอ key ใช้ได้ครั้งเดียว ลำดับนี้ทำให้รู้ก่อนว่ากล้องเปิดได้ ค่อยขอ key
81
+
82
+ ## State
83
+
84
+ ```
85
+ idle → previewing → connecting → live → stopping → stopped
86
+ ```
87
+
88
+ อ่านได้จาก `publisher.state` และ event `state` ใช้เปิด/ปิดปุ่มตาม state ถ้าเชื่อมต่อไม่สำเร็จหรือหลุดระหว่าง live จะกลับไป `previewing` โดยกล้องยังเปิด คนขายกดเริ่มใหม่ได้ทันที
89
+
90
+ ## API โดยย่อ
91
+
92
+ ### Method
93
+
94
+ | Method | ทำอะไร |
95
+ | -------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
96
+ | `startPreview({ video?, cameraId?, microphoneId?, videoConstraints?, mirror? })` | ขอสิทธิ์และเปิดกล้อง/ไมค์ แสดงใน `<video>` ถ้าส่งมา |
97
+ | `start({ getCredentials })` หรือ `start({ whipUrl, token? })` | เริ่ม live |
98
+ | `stop({ keepPreview? })` | จบ live (`keepPreview: true` = กล้องยังเปิด) หรือปิดกล้องตอน preview |
99
+ | `switchCamera(id?)`, `switchMicrophone(id?)`, `flipCamera()` | สลับอุปกรณ์ ใช้ได้ทั้งตอน preview และ live |
100
+ | `setCameraEnabled(bool)`, `setMicrophoneEnabled(bool)` | ปิด/เปิดภาพและเสียงโดยไม่ตัดการเชื่อมต่อ |
101
+ | `listDevices()` | รายการกล้องและไมค์ `{ cameras, microphones }` |
102
+ | `destroy()` | ปล่อยทุกอย่างถาวร เรียกตอนออกจากหน้า |
103
+ | `on(event, handler)`, `once(...)`, `off(...)` | สมัคร/เลิกรับ event |
104
+ | `checkSupport()` (ฟังก์ชันแยก) | เช็กความพร้อมของ browser ก่อนแสดงหน้า live |
105
+
106
+ ### ค่าที่อ่านได้
107
+
108
+ `state`, `stream`, `activeDevices`, `videoSettings` (ขนาดภาพจริง), `videoCodec` (codec ที่ใช้ระหว่าง live), `cameraEnabled`, `microphoneEnabled`
109
+
110
+ ### Event
111
+
112
+ | Event | ส่งเมื่อ |
113
+ | ----------------------------------- | ---------------------------------------------- |
114
+ | `state` | state เปลี่ยน `{ from, to }` |
115
+ | `live` | เริ่ม live สำเร็จ |
116
+ | `disconnected` | หลุดระหว่าง live |
117
+ | `devicechange` | เสียบหรือถอดอุปกรณ์ `{ cameras, microphones }` |
118
+ | `devicelost` | อุปกรณ์ที่ใช้อยู่หายไป `{ kind }` |
119
+ | `pagehidden` / `pagevisible` | หน้าเว็บถูกซ่อน / กลับมา (สลับแอป, ล็อกจอ) |
120
+ | `trackinterrupted` / `trackresumed` | ภาพหรือเสียงถูกพัก / กลับมา `{ kind }` |
121
+ | `error` | งานเบื้องหลังล้มเหลว (live ยังดำเนินต่อ) |
122
+
123
+ ### ค่าตั้ง (`new Publisher({...})`)
124
+
125
+ | ค่า | ค่าเริ่มต้น | ความหมาย |
126
+ | ---------------------- | ------------------- | -------------------------------------------------------- |
127
+ | `timeoutMs` | `15000` | เวลาสูงสุดที่ `start()` ใช้เชื่อมต่อ |
128
+ | `maxBitrateKbps` | อัตโนมัติตามขนาดภาพ | เพดานบิตเรตวิดีโอ |
129
+ | `iceServers` | ไม่มี | STUN/TURN สำหรับเครือข่ายที่ต่อตรงไม่ได้ |
130
+ | `autoSwitchMicrophone` | `true` | ไมค์ที่ใช้อยู่หลุด ให้สลับไปไมค์เริ่มต้นเอง |
131
+ | `videoCodecs` | `['H264', 'VP8']` | codec วิดีโอตามลำดับที่อยากใช้ (ไม่มี H.264 ใช้ VP8 แทน) |
132
+ | `allowInsecure` | `false` | อนุญาต WHIP URL แบบ http **ตอนพัฒนาเท่านั้น** |
133
+ | `keepScreenOn` | `true` | กันจอดับระหว่าง live (มือถือ) |
134
+
135
+ ## Error
136
+
137
+ ทุก error เป็น `PublisherError` ที่มี `code`, `message` (ภาษาอังกฤษสำหรับนักพัฒนา) และ `recoverable` (ลองใหม่แล้วอาจสำเร็จ) ใช้ `isPublisherError(error)` เช็ก และใช้ `code` แปลงเป็นข้อความที่จะแสดงคนขายเอง `error.toJSON()` ส่งเข้าระบบ log ได้โดยไม่มี stream key
138
+
139
+ ## Browser ที่รองรับ
140
+
141
+ Chrome, Edge, Firefox และ Safari รุ่นปัจจุบันบนคอมพิวเตอร์, Safari บน iOS และ Chrome บน Android browser ในแอปแชท (LINE, Facebook ฯลฯ) หลายตัวไม่ให้ใช้กล้อง ใช้ `checkSupport()` เช็กก่อนเสมอ
142
+
143
+ ## ความปลอดภัย
144
+
145
+ - **ห้ามเขียน stream key หรือ WHIP URL ไว้ในโค้ดหน้าเว็บ** ให้ backend ของร้านขอจาก API ของระบบ live แล้วส่งให้ผ่าน `getCredentials` เฉพาะคนขายที่ login แล้ว
146
+ - WHIP URL ต้องเป็น HTTPS: `http://` ไปยัง host จริงได้ `INSECURE_URL` และหน้า HTTPS ส่งไป http ได้ `MIXED_CONTENT` (`allowInsecure: true` ใช้ได้ตอนพัฒนาบน localhost เท่านั้น)
147
+ - library ไม่พิมพ์ stream key หรือ URL เต็มที่ไหนเลย คำเตือนแสดงแค่ protocol และ host
148
+
149
+ ## คู่มือละเอียด
150
+
151
+ | คู่มือ | เนื้อหา |
152
+ | -------------------------------------------------- | ---------------------------------------------------------- |
153
+ | [Preview และอุปกรณ์](docs/guide/preview.md) | เปิดกล้อง, แสดงภาพ, ความละเอียด, เลือกและสลับอุปกรณ์ |
154
+ | [Live](docs/guide/live.md) | credentials, เริ่ม/จบ live, mute, บิตเรต, ตัวอย่างครบ flow |
155
+ | [State และ event](docs/guide/state-and-events.md) | state ทั้งหมดพร้อม diagram และ event |
156
+ | [Error codes](docs/guide/error-codes.md) | ความหมายของทุก code และสิ่งที่แอปควรทำ |
157
+ | [Browser ที่รองรับ](docs/guide/browser-support.md) | `checkSupport()` และข้อจำกัดของแต่ละ browser |
158
+
159
+ ตัวอย่างที่รันได้จริงอยู่ใน [examples/](examples/) ใช้ React หรือ Vue คัดลอก `usePublisher` จาก [examples/react](examples/react/README.md) หรือ [examples/vue](examples/vue/README.md) ไปใช้ได้เลย (สร้างและทำลาย publisher ตามอายุของ component ให้)
160
+
161
+ ## พัฒนา library นี้
162
+
163
+ ดู [CONTRIBUTING.md](CONTRIBUTING.md) และเอกสารทั้งหมดของโปรเจกต์ที่ [docs/](docs/README.md)
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Video codecs the app can ask for, by their common names.
3
+ * codec วิดีโอที่แอปเลือกได้ ตามชื่อที่ใช้กันทั่วไป
4
+ */
5
+ export declare const VIDEO_CODECS: readonly ["H264", "VP8", "VP9", "AV1"];
6
+ export type VideoCodec = (typeof VIDEO_CODECS)[number];
7
+ /**
8
+ * Used when the app gives no list: H.264 first (the server handles it best), VP8 when the device
9
+ * cannot send H.264.
10
+ *
11
+ * ใช้เมื่อแอปไม่ได้ส่งรายการมา: H.264 ก่อน (server รองรับดีที่สุด) และ VP8 เมื่อเครื่องส่ง H.264 ไม่ได้
12
+ */
13
+ export declare const DEFAULT_VIDEO_CODECS: readonly VideoCodec[];
14
+ /**
15
+ * The codec of a MIME type such as `video/H264`, or undefined for others (`video/rtx`, audio…).
16
+ * codec ของ MIME type เช่น `video/H264` หรือ undefined ถ้าเป็นอย่างอื่น (`video/rtx`, เสียง ฯลฯ)
17
+ */
18
+ export declare function codecOfMimeType(mimeType: string): VideoCodec | undefined;
19
+ /**
20
+ * Checks the app's `videoCodecs` option: a non-empty list of known codecs without repeats.
21
+ * Throws `INVALID_OPTIONS` otherwise.
22
+ *
23
+ * ตรวจ option `videoCodecs` ของแอป: ต้องเป็นรายการที่ไม่ว่าง มีแต่ codec ที่รู้จัก และไม่ซ้ำ
24
+ * ถ้าไม่ใช่จะ throw `INVALID_OPTIONS`
25
+ */
26
+ export declare function validateVideoCodecs(value: unknown): VideoCodec[];
27
+ /**
28
+ * Codecs this browser can send, in the browser's order, from
29
+ * `RTCRtpSender.getCapabilities('video')`. `'unknown'` when the browser has no such API.
30
+ *
31
+ * codec ที่ browser นี้ส่งได้ เรียงตามที่ browser ให้มา จาก `RTCRtpSender.getCapabilities('video')`
32
+ * ได้ `'unknown'` ถ้า browser ไม่มี API นี้
33
+ */
34
+ export declare function availableVideoCodecs(capabilities: RTCRtpCapabilities | null | undefined): VideoCodec[] | 'unknown';
35
+ /**
36
+ * Builds the list for `RTCRtpTransceiver.setCodecPreferences()`: the wanted codecs in the app's
37
+ * order, then RTX (retransmission). RED and ULPFEC are left out: the server does not use them.
38
+ * Throws `CODEC_NOT_SUPPORTED` when the browser can send none of the wanted codecs.
39
+ *
40
+ * สร้างรายการสำหรับ `RTCRtpTransceiver.setCodecPreferences()`: codec ที่ต้องการเรียงตามลำดับของแอป
41
+ * ตามด้วย RTX (ส่งข้อมูลที่หายซ้ำ) ไม่ใส่ RED และ ULPFEC เพราะ server ไม่ได้ใช้
42
+ * throw `CODEC_NOT_SUPPORTED` ถ้า browser ส่ง codec ที่ต้องการไม่ได้เลยสักตัว
43
+ */
44
+ export declare function codecPreferences(capabilities: readonly RTCRtpCodec[], wanted: readonly VideoCodec[]): RTCRtpCodec[];
45
+ /**
46
+ * Fallback for browsers without `setCodecPreferences`: rewrites the offer sent to the server so
47
+ * its video sections list only the wanted codecs. The browser's own local description is left
48
+ * untouched; the server answers with a subset, so the browser must send one of them.
49
+ * Throws `CODEC_NOT_SUPPORTED` when the offer contains none of the wanted codecs.
50
+ *
51
+ * ทางสำรองสำหรับ browser ที่ไม่มี `setCodecPreferences`: แก้ offer ที่ส่งไป server ให้ส่วนวิดีโอ
52
+ * มีแค่ codec ที่ต้องการ ไม่แตะ local description ของ browser เอง server จะตอบกลับด้วย codec
53
+ * ในรายการนี้ browser จึงต้องส่งตัวใดตัวหนึ่งในนั้น
54
+ * throw `CODEC_NOT_SUPPORTED` ถ้า offer ไม่มี codec ที่ต้องการเลย
55
+ */
56
+ export declare function filterOfferSdp(sdp: string, wanted: readonly VideoCodec[]): string;
57
+ /**
58
+ * The video codec the server chose: the first payload type of the answer's `m=video` line.
59
+ * Undefined when there is no video section, it was rejected (port 0), or the codec is unknown.
60
+ *
61
+ * codec วิดีโอที่ server เลือก: payload type ตัวแรกในบรรทัด `m=video` ของ answer
62
+ * ได้ undefined ถ้าไม่มีส่วนวิดีโอ ถูกปฏิเสธ (port 0) หรือเป็น codec ที่ไม่รู้จัก
63
+ */
64
+ export declare function negotiatedVideoCodec(answerSdp: string): VideoCodec | undefined;
@@ -0,0 +1,287 @@
1
+ import { PublisherError } from './errors';
2
+ import { MediaKind, MediaKindEvent } from './events';
3
+ /**
4
+ * One camera or microphone. `label` is empty until the user has granted permission.
5
+ * กล้องหรือไมโครโฟน 1 ตัว `label` จะว่างจนกว่าผู้ใช้จะอนุญาตสิทธิ์
6
+ */
7
+ export interface DeviceInfo {
8
+ /**
9
+ * Pass this to `startPreview()` or `switchCamera()` / `switchMicrophone()`.
10
+ * ใช้ส่งให้ `startPreview()` หรือ `switchCamera()` / `switchMicrophone()`
11
+ */
12
+ deviceId: string;
13
+ /**
14
+ * Human-readable name, e.g. "MacBook Camera". Empty before permission is granted.
15
+ * ชื่อที่อ่านได้ เช่น "MacBook Camera" ว่างถ้ายังไม่ได้สิทธิ์
16
+ */
17
+ label: string;
18
+ /**
19
+ * `video` for a camera, `audio` for a microphone.
20
+ * `video` คือกล้อง `audio` คือไมโครโฟน
21
+ */
22
+ kind: MediaKind;
23
+ }
24
+ /**
25
+ * Available cameras and microphones. Speakers are not included.
26
+ * กล้องและไมโครโฟนที่มี ไม่รวมลำโพง
27
+ */
28
+ export interface DeviceList {
29
+ cameras: DeviceInfo[];
30
+ microphones: DeviceInfo[];
31
+ }
32
+ /**
33
+ * Which devices to open. Omitted or no longer present → the browser's default device.
34
+ * อุปกรณ์ที่จะเปิด ถ้าไม่ระบุหรือไม่มีอยู่แล้ว จะใช้อุปกรณ์เริ่มต้นของ browser
35
+ */
36
+ export interface MediaSelection {
37
+ cameraId?: string;
38
+ microphoneId?: string;
39
+ }
40
+ /**
41
+ * The browser APIs the device manager uses. Tests pass a fake.
42
+ * API ของ browser ที่ตัวจัดการอุปกรณ์ใช้ test ส่งของจำลองเข้ามา
43
+ */
44
+ export interface DevicesEnv {
45
+ /**
46
+ * `window.isSecureContext`: true on HTTPS or localhost. Browsers hide the camera otherwise.
47
+ * `window.isSecureContext`: true เมื่อเปิดผ่าน HTTPS หรือ localhost ถ้าไม่ใช่ browser จะซ่อนกล้อง
48
+ */
49
+ isSecureContext?: boolean;
50
+ mediaDevices?: Partial<Pick<MediaDevices, 'getUserMedia' | 'enumerateDevices' | 'addEventListener' | 'removeEventListener'>>;
51
+ }
52
+ /**
53
+ * The real browser APIs. Safe where `navigator` is missing (server-side rendering).
54
+ * API จาก browser จริง ใช้ได้แม้ไม่มี `navigator` (render บน server)
55
+ */
56
+ export declare function browserDevicesEnv(): DevicesEnv;
57
+ /**
58
+ * Lists cameras and microphones. Never shows a permission prompt.
59
+ * Throws `MEDIA_DEVICES_NOT_SUPPORTED` if the browser exposes no media devices.
60
+ *
61
+ * รายการกล้องและไมโครโฟน ไม่ทำให้หน้าขอสิทธิ์ขึ้น
62
+ * throw `MEDIA_DEVICES_NOT_SUPPORTED` ถ้า browser ไม่เปิดให้เข้าถึงอุปกรณ์
63
+ */
64
+ export declare function listDevices(env: DevicesEnv): Promise<DeviceList>;
65
+ /**
66
+ * Which device each open track uses.
67
+ * อุปกรณ์ที่ track แต่ละตัวกำลังใช้
68
+ */
69
+ export interface ActiveDevices {
70
+ cameraId: string | undefined;
71
+ microphoneId: string | undefined;
72
+ }
73
+ /**
74
+ * Callbacks from the device manager to its owner.
75
+ * callback ที่ตัวจัดการอุปกรณ์เรียกกลับไปหาผู้ใช้คลาส
76
+ */
77
+ export interface DeviceManagerCallbacks {
78
+ /**
79
+ * A track in the stream was replaced (after a switch, or when the old device was restored).
80
+ * The owner should send the new track, e.g. with `RTCRtpSender.replaceTrack()`.
81
+ *
82
+ * track ใน stream ถูกเปลี่ยน (หลังสลับ หรือหลังเปิดอุปกรณ์เดิมกลับมา)
83
+ * ผู้ใช้คลาสควรส่ง track ใหม่ต่อ เช่น ด้วย `RTCRtpSender.replaceTrack()`
84
+ */
85
+ onTrackReplaced?: (track: MediaStreamTrack) => void;
86
+ /**
87
+ * A device was plugged in or removed. Becomes the `devicechange` event.
88
+ * มีการเสียบหรือถอดอุปกรณ์ กลายเป็น event `devicechange`
89
+ */
90
+ onDeviceChange?: (devices: DeviceList) => void;
91
+ /**
92
+ * A device in use disappeared (unplugged, or permission revoked). Becomes `devicelost`.
93
+ * อุปกรณ์ที่กำลังใช้หายไป (ถอดออก หรือถูกถอนสิทธิ์) กลายเป็น event `devicelost`
94
+ */
95
+ onDeviceLost?: (event: MediaKindEvent) => void;
96
+ /**
97
+ * Background work failed, e.g. the automatic microphone switch. Becomes `error`.
98
+ * งานเบื้องหลังล้มเหลว เช่น การสลับไมโครโฟนอัตโนมัติ กลายเป็น event `error`
99
+ */
100
+ onError?: (error: PublisherError) => void;
101
+ /**
102
+ * A track in use stopped delivering for now: the OS paused it (e.g. an incoming call, or the
103
+ * page was hidden on a phone). Becomes `trackinterrupted`.
104
+ *
105
+ * track ที่ใช้อยู่หยุดส่งข้อมูลชั่วคราว: OS พักไว้ (เช่น สายโทรเข้า หรือหน้าเว็บถูกซ่อนบนมือถือ)
106
+ * กลายเป็น event `trackinterrupted`
107
+ */
108
+ onTrackMuted?: (event: MediaKindEvent) => void;
109
+ /**
110
+ * A paused track delivers again. Becomes `trackresumed`.
111
+ * track ที่ถูกพักกลับมาส่งข้อมูลอีกครั้ง กลายเป็น event `trackresumed`
112
+ */
113
+ onTrackUnmuted?: (event: MediaKindEvent) => void;
114
+ }
115
+ /**
116
+ * Settings for the device manager.
117
+ * ค่าตั้งของตัวจัดการอุปกรณ์
118
+ */
119
+ export interface DeviceManagerOptions {
120
+ /**
121
+ * When the microphone in use disappears, switch to the default microphone. Default `true`.
122
+ * The camera is never switched automatically; the page decides.
123
+ *
124
+ * เมื่อไมโครโฟนที่ใช้อยู่หายไป ให้สลับไปไมโครโฟนเริ่มต้นเอง ค่าเริ่มต้น `true`
125
+ * กล้องจะไม่สลับเอง ให้หน้าเว็บตัดสินใจ
126
+ */
127
+ autoSwitchMicrophone?: boolean;
128
+ /**
129
+ * Whether the page is hidden. A track that ends while hidden was most likely stopped by a
130
+ * mobile OS, not unplugged: it is reported as interrupted and reopened by `recover()`.
131
+ * Default: never hidden.
132
+ *
133
+ * หน้าเว็บถูกซ่อนอยู่ไหม track ที่จบตอนหน้าซ่อนมักเป็นเพราะ OS ของมือถือหยุดไว้ ไม่ใช่ถูกถอด
134
+ * จึงแจ้งเป็นถูกพัก และ `recover()` จะเปิดกลับ ค่าเริ่มต้น: ไม่ซ่อนเลย
135
+ */
136
+ isPageHidden?: () => boolean;
137
+ }
138
+ /**
139
+ * Result of `recover()`.
140
+ * ผลของ `recover()`
141
+ */
142
+ export interface RecoverResult {
143
+ recovered: MediaKind[];
144
+ failed: {
145
+ kind: MediaKind;
146
+ error: PublisherError;
147
+ }[];
148
+ }
149
+ /**
150
+ * Internal owner of the camera/microphone stream. Separate from the transport so each can be
151
+ * tested and replaced on its own. `Publisher` uses it and exposes the public methods.
152
+ *
153
+ * เจ้าของ stream ของกล้อง/ไมโครโฟนภายใน library แยกจากส่วนส่งข้อมูล เพื่อ test และเปลี่ยนได้อิสระ
154
+ * `Publisher` จะเรียกใช้และเปิด method ให้ลูกค้า
155
+ */
156
+ export declare class DeviceManager {
157
+ private readonly env;
158
+ private readonly callbacks;
159
+ private current;
160
+ private videoConstraints;
161
+ private watching;
162
+ private readonly autoSwitchMicrophone;
163
+ private readonly isPageHidden;
164
+ private readonly paused;
165
+ constructor(env: DevicesEnv, callbacks?: DeviceManagerCallbacks, options?: DeviceManagerOptions);
166
+ /**
167
+ * The open stream, or null.
168
+ * stream ที่เปิดอยู่ หรือ null
169
+ */
170
+ get stream(): MediaStream | null;
171
+ /**
172
+ * Device ids of the open camera and microphone, as reported by the browser.
173
+ * device id ของกล้องและไมโครโฟนที่เปิดอยู่ ตามที่ browser บอก
174
+ */
175
+ get active(): ActiveDevices;
176
+ /**
177
+ * Opens a camera and a microphone (both are required in v1). Closes any stream opened before.
178
+ * An id that is no longer present falls back to the default device of that kind only;
179
+ * the other device keeps the one that was selected.
180
+ *
181
+ * เปิดกล้องและไมโครโฟน (v1 ต้องมีทั้งสองอย่าง) และปิด stream เดิมถ้าเปิดไว้ก่อน
182
+ * id ที่ไม่มีอยู่แล้วจะใช้อุปกรณ์เริ่มต้นแทนเฉพาะชนิดนั้น อีกตัวยังใช้ตัวที่เลือกไว้
183
+ */
184
+ open(selection?: MediaSelection, videoConstraints?: MediaTrackConstraints): Promise<MediaStream>;
185
+ /**
186
+ * Stops every track (the camera light turns off). Safe to call when nothing is open.
187
+ * หยุด track ทั้งหมด (ไฟกล้องดับ) เรียกได้แม้ไม่ได้เปิดอะไรไว้
188
+ */
189
+ close(): void;
190
+ /**
191
+ * Reopens the devices whose track ended while the page was hidden (call when it is visible
192
+ * again). The same device first; if it is gone, the default microphone when
193
+ * `autoSwitchMicrophone` is on, but never another camera. New tracks reach the connection
194
+ * through `onTrackReplaced`, so the live stream continues without reconnecting.
195
+ *
196
+ * เปิดอุปกรณ์ที่ track จบตอนหน้าซ่อนกลับมา (เรียกเมื่อหน้าแสดงอีกครั้ง) ลองตัวเดิมก่อน
197
+ * ถ้าไม่มีแล้ว ไมค์จะใช้ตัวเริ่มต้นเมื่อเปิด `autoSwitchMicrophone` แต่กล้องไม่เปลี่ยนเป็นตัวอื่น
198
+ * track ใหม่ส่งถึง connection ผ่าน `onTrackReplaced` live จึงต่อได้โดยไม่ต้องเชื่อมต่อใหม่
199
+ */
200
+ recover(): Promise<RecoverResult>;
201
+ /**
202
+ * Closes the stream and stops listening for device changes. Call when the publisher is
203
+ * destroyed. Safe to call more than once.
204
+ *
205
+ * ปิด stream และเลิกฟังการเปลี่ยนอุปกรณ์ เรียกตอนทำลาย publisher เรียกซ้ำได้
206
+ */
207
+ dispose(): void;
208
+ /**
209
+ * Switches to another camera, or to the default camera when `deviceId` is omitted.
210
+ * Keeps the video constraints given to `open()`. An unknown id throws `DEVICE_NOT_FOUND`
211
+ * and keeps the current camera.
212
+ *
213
+ * สลับไปกล้องตัวอื่น หรือกล้องเริ่มต้นถ้าไม่ระบุ `deviceId` ใช้ค่าวิดีโอเดิมจาก `open()`
214
+ * id ที่ไม่มีจะ throw `DEVICE_NOT_FOUND` และคงกล้องเดิมไว้
215
+ */
216
+ switchCamera(deviceId?: string): Promise<MediaStreamTrack>;
217
+ /**
218
+ * Switches to another microphone, or to the default microphone when `deviceId` is omitted.
219
+ * สลับไปไมโครโฟนตัวอื่น หรือไมโครโฟนเริ่มต้นถ้าไม่ระบุ `deviceId`
220
+ */
221
+ switchMicrophone(deviceId?: string): Promise<MediaStreamTrack>;
222
+ /**
223
+ * Switches between the front (`user`) and back (`environment`) camera. On a device with
224
+ * one camera this throws `CONSTRAINTS_NOT_SATISFIED` and keeps the current camera.
225
+ *
226
+ * สลับกล้องหน้า (`user`) และกล้องหลัง (`environment`) เครื่องที่มีกล้องตัวเดียวจะ throw
227
+ * `CONSTRAINTS_NOT_SATISFIED` และคงกล้องเดิมไว้
228
+ */
229
+ flipCamera(): Promise<MediaStreamTrack>;
230
+ /**
231
+ * Opens a new track of `kind` and swaps it into the stream.
232
+ * Desktop: the new device opens first, so the picture never goes blank.
233
+ * Mobile (one camera at a time): on DEVICE_IN_USE, stop the old track and try again.
234
+ * If that also fails, reopen the old device so the seller is never left without one.
235
+ *
236
+ * เปิด track ชนิด `kind` ตัวใหม่แล้วใส่แทนใน stream
237
+ * คอมพิวเตอร์: เปิดอุปกรณ์ใหม่ก่อน ภาพจึงไม่ดับ
238
+ * มือถือ (เปิดกล้องได้ทีละตัว): ถ้าได้ DEVICE_IN_USE ให้ปิดตัวเก่าแล้วลองใหม่
239
+ * ถ้ายังไม่ได้ เปิดอุปกรณ์เดิมกลับมา คนขายจะได้ไม่ขาดอุปกรณ์
240
+ */
241
+ private replace;
242
+ /**
243
+ * Best effort: reopen the device that was stopped for a failed switch.
244
+ * พยายามเปิดอุปกรณ์ที่ถูกปิดไประหว่างการสลับที่ไม่สำเร็จกลับมา
245
+ */
246
+ private restore;
247
+ /**
248
+ * Reacts when `track` ends by itself. Browsers do not fire `ended` after our own `stop()`,
249
+ * and a track that is no longer in the stream is ignored, so no listener needs removing.
250
+ *
251
+ * ตอบสนองเมื่อ `track` จบเอง browser ไม่ส่ง `ended` หลังเราเรียก `stop()` เอง
252
+ * และ track ที่ไม่อยู่ใน stream แล้วจะถูกข้าม จึงไม่ต้องถอด listener
253
+ */
254
+ private watchTrack;
255
+ private watchDevices;
256
+ private readonly handleDeviceChange;
257
+ private videoRequest;
258
+ /**
259
+ * Drops each selected id that the device list shows is gone. Keeps an id when the list
260
+ * cannot tell: no permission yet (ids are empty), or `enumerateDevices` is missing or fails.
261
+ *
262
+ * ตัด id ที่เลือกไว้ซึ่งรายการอุปกรณ์บอกว่าไม่อยู่แล้ว คง id ไว้ถ้ารายการบอกไม่ได้
263
+ * เช่น ยังไม่ได้สิทธิ์ (id เป็นค่าว่าง) หรือ `enumerateDevices` ไม่มีหรือ error
264
+ */
265
+ private presentOnly;
266
+ /**
267
+ * Opens the camera, then the microphone, each falling back to its own default device.
268
+ * If the microphone fails, the camera is stopped so nothing is left on.
269
+ *
270
+ * เปิดกล้องแล้วค่อยเปิดไมโครโฟน แต่ละตัวถอยไปอุปกรณ์เริ่มต้นของตัวเองได้
271
+ * ถ้าไมโครโฟนพัง จะปิดกล้องด้วย ไม่ให้มีอะไรค้างเปิดอยู่
272
+ */
273
+ private openSeparately;
274
+ /**
275
+ * Requests one kind of media with `deviceId`; if that id is not available, requests the
276
+ * default device of the same kind. Other errors (permission, device in use) are thrown as is.
277
+ *
278
+ * ขอสื่อชนิดเดียวด้วย `deviceId` ถ้าหา id นั้นไม่เจอ จะขออุปกรณ์เริ่มต้นของชนิดเดียวกันแทน
279
+ * error อื่น (สิทธิ์, อุปกรณ์ถูกใช้อยู่) จะ throw ต่อตามเดิม
280
+ */
281
+ private requestWithFallback;
282
+ /**
283
+ * Requests one kind of media. Errors come back as `PublisherError`.
284
+ * ขอสื่อชนิดเดียว error ที่ได้จะเป็น `PublisherError` เสมอ
285
+ */
286
+ private request;
287
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * A listener receives the arguments declared for its event.
3
+ * handler จะได้รับ argument ตามที่ประกาศไว้ของ event นั้น
4
+ */
5
+ export type Listener<Args extends unknown[]> = (...args: Args) => void;
6
+ /**
7
+ * Small typed event emitter used internally. `Events` maps each event name to its listener
8
+ * arguments, e.g. `{ state: [change: StateChange]; live: [] }`.
9
+ *
10
+ * - A listener that throws does not stop the others; its error goes to `onListenerError`.
11
+ * - Adding the same listener twice for one event is a no-op.
12
+ * - A listener removed during an emit is not called if it has not run yet.
13
+ * - A listener added during an emit is first called on the next emit.
14
+ *
15
+ * ระบบ event แบบมี type ที่ใช้ภายใน library `Events` จับคู่ชื่อ event กับ argument ของ handler
16
+ * เช่น `{ state: [change: StateChange]; live: [] }`
17
+ *
18
+ * - handler ที่ throw ไม่ทำให้ตัวอื่นหยุด error ของมันถูกส่งไปที่ `onListenerError`
19
+ * - สมัคร handler ตัวเดิมซ้ำใน event เดียวกันจะไม่มีผลอะไร
20
+ * - handler ที่ถูกเลิกรับระหว่างกำลังส่ง event จะไม่ถูกเรียก ถ้ายังไม่ถึงคิว
21
+ * - handler ที่สมัครระหว่างกำลังส่ง event จะเริ่มได้รับตั้งแต่ event ถัดไป
22
+ */
23
+ export declare class Emitter<Events extends Record<string, unknown[]>> {
24
+ private readonly onListenerError;
25
+ private readonly entries;
26
+ constructor(onListenerError?: (error: unknown) => void);
27
+ /**
28
+ * Subscribes to `event`. Returns a function that unsubscribes.
29
+ * สมัครรับ `event` คืนฟังก์ชันสำหรับเลิกรับ
30
+ */
31
+ on<K extends keyof Events>(event: K, listener: Listener<Events[K]>): () => void;
32
+ /**
33
+ * Subscribes to the next `event` only. Returns a function that unsubscribes.
34
+ * สมัครรับ `event` ครั้งถัดไปครั้งเดียว คืนฟังก์ชันสำหรับเลิกรับ
35
+ */
36
+ once<K extends keyof Events>(event: K, listener: Listener<Events[K]>): () => void;
37
+ /**
38
+ * Unsubscribes `listener` from `event`. Does nothing if it was not subscribed.
39
+ * เลิกรับ `listener` จาก `event` ถ้าไม่ได้สมัครไว้ก็ไม่ทำอะไร
40
+ */
41
+ off<K extends keyof Events>(event: K, listener: Listener<Events[K]>): void;
42
+ /**
43
+ * Calls every listener of `event` with `args`, in subscription order.
44
+ * เรียก handler ทุกตัวของ `event` ด้วย `args` ตามลำดับที่สมัคร
45
+ */
46
+ emit<K extends keyof Events>(event: K, ...args: Events[K]): void;
47
+ /**
48
+ * Removes every listener of every event (used by `destroy()`).
49
+ * ลบ handler ทั้งหมดของทุก event (ใช้ตอน `destroy()`)
50
+ */
51
+ removeAll(): void;
52
+ private add;
53
+ }