@apocaliss92/nodelink-js 0.1.8 → 0.1.9

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/dist/index.d.ts CHANGED
@@ -919,6 +919,84 @@ export declare type BaichuanHeader = {
919
919
  payloadOffset?: number;
920
920
  };
921
921
 
922
+ export declare class BaichuanHlsServer extends EventEmitter {
923
+ private readonly api;
924
+ private readonly channel;
925
+ private readonly profile;
926
+ private readonly variant;
927
+ private readonly segmentDuration;
928
+ private readonly playlistSize;
929
+ private readonly ffmpegPath;
930
+ private readonly log;
931
+ private outputDir;
932
+ private createdTempDir;
933
+ private playlistPath;
934
+ private segmentPattern;
935
+ private state;
936
+ private codec;
937
+ private framesReceived;
938
+ private ffmpeg;
939
+ private nativeStream;
940
+ private pumpPromise;
941
+ private startedAt;
942
+ private lastError;
943
+ constructor(options: BaichuanHlsServerOptions);
944
+ /**
945
+ * Start HLS streaming
946
+ */
947
+ start(): Promise<void>;
948
+ /**
949
+ * Stop HLS streaming
950
+ */
951
+ stop(): Promise<void>;
952
+ /**
953
+ * Get current status
954
+ */
955
+ getStatus(): HlsServerStatus;
956
+ /**
957
+ * Get playlist file path
958
+ */
959
+ getPlaylistPath(): string | null;
960
+ /**
961
+ * Get output directory
962
+ */
963
+ getOutputDir(): string | null;
964
+ /**
965
+ * Check if playlist file exists
966
+ */
967
+ waitForPlaylist(timeoutMs?: number): Promise<boolean>;
968
+ /**
969
+ * Read an HLS asset (playlist or segment)
970
+ */
971
+ readAsset(assetName: string): Promise<{
972
+ data: Buffer;
973
+ contentType: string;
974
+ } | null>;
975
+ private pumpNativeToFfmpeg;
976
+ private spawnFfmpeg;
977
+ }
978
+
979
+ export declare interface BaichuanHlsServerOptions {
980
+ /** API instance (required) */
981
+ api: ReolinkBaichuanApi;
982
+ /** Channel number (required) */
983
+ channel: number;
984
+ /** Stream profile (required) */
985
+ profile: StreamProfile;
986
+ /** Native-only: TrackMix tele/autotrack variants */
987
+ variant?: NativeVideoStreamVariant;
988
+ /** Output directory for HLS segments. If not provided, a temp directory will be created. */
989
+ outputDir?: string;
990
+ /** HLS segment duration in seconds (default: 2) */
991
+ segmentDuration?: number;
992
+ /** Number of segments to keep in playlist (default: 5) */
993
+ playlistSize?: number;
994
+ /** ffmpeg binary path (default: "ffmpeg") */
995
+ ffmpegPath?: string;
996
+ /** Logger callback */
997
+ logger?: (level: "debug" | "info" | "warn" | "error", message: string) => void;
998
+ }
999
+
922
1000
  /**
923
1001
  * BaichuanHttpStreamServer - HTTP server that serves a Baichuan video stream as MPEG-TS.
924
1002
  *
@@ -1549,8 +1627,16 @@ export declare class BaichuanWebRTCServer extends EventEmitter {
1549
1627
  private pumpFramesToWebRTC;
1550
1628
  /**
1551
1629
  * Send H.264 frame via RTP media track
1630
+ * Returns the number of RTP packets sent
1552
1631
  */
1553
1632
  private sendH264Frame;
1633
+ /**
1634
+ * Send video frame via DataChannel (works for both H.264 and H.265)
1635
+ * Format: 12-byte header + Annex-B data
1636
+ * Header: [frameNum (4)] [timestamp (4)] [flags (1)] [keyframe (1)] [reserved (2)]
1637
+ * Flags: 0x01 = H.265, 0x02 = H.264
1638
+ */
1639
+ private sendVideoFrameViaDataChannel;
1554
1640
  /**
1555
1641
  * Send H.265 frame via DataChannel
1556
1642
  * Format: 12-byte header + Annex-B data
@@ -2178,6 +2264,14 @@ export declare function buildChannelExtensionXml(channelId: number | string | un
2178
2264
  */
2179
2265
  export declare function buildFloodlightManualXml(channelId: number, status: number, durationSeconds?: number): string;
2180
2266
 
2267
+ /**
2268
+ * Build the HLS redirect URL from the original request URL.
2269
+ *
2270
+ * @param originalUrl - The original request URL
2271
+ * @returns The URL with ?hls=playlist.m3u8 appended
2272
+ */
2273
+ export declare function buildHlsRedirectUrl(originalUrl: string): string;
2274
+
2181
2275
  export declare function buildLoginXml(userNameHash: string, passwordHash: string): string;
2182
2276
 
2183
2277
  /**
@@ -3171,8 +3265,8 @@ export declare type DebugOptions = {
3171
3265
  * - Firefox: No H.265 support, needs transcoding
3172
3266
  * - Android: Variable support, transcode for safety
3173
3267
  *
3174
- * @param headers HTTP request headers
3175
- * @param forceMode Optional override: "passthrough" or "transcode-h264"
3268
+ * @param headers - HTTP request headers
3269
+ * @param forceMode - Optional override: "passthrough" or "transcode-h264"
3176
3270
  * @returns Decision with mode, reason, and client info
3177
3271
  */
3178
3272
  export declare function decideVideoclipTranscodeMode(headers: Record<string, string | string[] | undefined>, forceMode?: VideoclipTranscodeMode): VideoclipModeDecision;
@@ -3190,6 +3284,18 @@ export declare function decodeHeader(buf: AnyBuffer): {
3190
3284
  */
3191
3285
  export declare function deriveAesKey(nonce: string, password: string): Buffer;
3192
3286
 
3287
+ /**
3288
+ * Detect if the request is from an iOS device that needs HLS.
3289
+ *
3290
+ * @param userAgent - The User-Agent header from the request
3291
+ * @returns Object with iOS detection results
3292
+ */
3293
+ export declare function detectIosClient(userAgent: string | undefined): {
3294
+ isIos: boolean;
3295
+ isIosInstalledApp: boolean;
3296
+ needsHls: boolean;
3297
+ };
3298
+
3193
3299
  /**
3194
3300
  * Detect the actual video codec from raw NAL data.
3195
3301
  * Some cameras report wrong codec (e.g. "H264" but send H.265 data).
@@ -3802,6 +3908,145 @@ export declare interface HddInfoListConfig {
3802
3908
  };
3803
3909
  }
3804
3910
 
3911
+ export declare type HlsCodec = "h264" | "h265";
3912
+
3913
+ /**
3914
+ * HTTP response result.
3915
+ */
3916
+ export declare interface HlsHttpResponse {
3917
+ /** HTTP status code */
3918
+ statusCode: number;
3919
+ /** Response headers */
3920
+ headers: Record<string, string>;
3921
+ /** Response body (string for playlist, Buffer for segment) */
3922
+ body: string | Buffer;
3923
+ }
3924
+
3925
+ export declare interface HlsServerStatus {
3926
+ state: "idle" | "starting" | "running" | "stopping" | "stopped" | "error";
3927
+ codec: HlsCodec | null;
3928
+ framesReceived: number;
3929
+ ffmpegRunning: boolean;
3930
+ playlistPath: string | null;
3931
+ outputDir: string | null;
3932
+ startedAt: Date | null;
3933
+ error: string | null;
3934
+ }
3935
+
3936
+ /**
3937
+ * HLS session returned by createRecordingReplayHlsSession.
3938
+ */
3939
+ export declare interface HlsSession {
3940
+ /** Get the current HLS playlist content (.m3u8) */
3941
+ getPlaylist: () => string;
3942
+ /** Get a segment file by name */
3943
+ getSegment: (name: string) => Buffer | undefined;
3944
+ /** List all available segment names */
3945
+ listSegments: () => string[];
3946
+ /** Wait for the HLS session to be ready */
3947
+ waitForReady: () => Promise<void>;
3948
+ /** Stop the HLS session and cleanup */
3949
+ stop: () => Promise<void>;
3950
+ /** Path to the temporary directory */
3951
+ tempDir: string;
3952
+ }
3953
+
3954
+ /**
3955
+ * Manages HLS sessions with caching, TTL, and HTTP response generation.
3956
+ */
3957
+ export declare class HlsSessionManager {
3958
+ private readonly api;
3959
+ private sessions;
3960
+ private readonly logger;
3961
+ private readonly sessionTtlMs;
3962
+ private cleanupTimer;
3963
+ private creationLocks;
3964
+ constructor(api: ReolinkBaichuanApi, options?: HlsSessionManagerOptions);
3965
+ /**
3966
+ * Handle an HLS request and return the HTTP response.
3967
+ *
3968
+ * @param params - Request parameters
3969
+ * @returns HTTP response ready to be sent
3970
+ */
3971
+ handleRequest(params: {
3972
+ /** Unique session key (e.g., `${deviceId}:${fileId}`) */
3973
+ sessionKey: string;
3974
+ /** HLS path: "playlist.m3u8" or segment name like "segment_001.ts" */
3975
+ hlsPath: string;
3976
+ /** Full request URL for rewriting playlist URLs */
3977
+ requestUrl: string;
3978
+ /** Function to create session params if session doesn't exist */
3979
+ createSession: () => Promise<HlsSessionParams> | HlsSessionParams;
3980
+ /**
3981
+ * Optional prefix used to ensure only one active HLS session per logical client.
3982
+ * When a new session is created, any other sessions whose keys start with this
3983
+ * prefix will be stopped. This prevents replay/ffmpeg queue starvation when
3984
+ * clients quickly switch clips.
3985
+ */
3986
+ exclusiveKeyPrefix?: string;
3987
+ }): Promise<HlsHttpResponse>;
3988
+ private withCreationLock;
3989
+ /**
3990
+ * Check if a session exists for the given key.
3991
+ */
3992
+ hasSession(sessionKey: string): boolean;
3993
+ /**
3994
+ * Stop a specific session.
3995
+ */
3996
+ stopSession(sessionKey: string): Promise<void>;
3997
+ /**
3998
+ * Stop all sessions and cleanup.
3999
+ */
4000
+ stopAll(): Promise<void>;
4001
+ /**
4002
+ * Get the number of active sessions.
4003
+ */
4004
+ get sessionCount(): number;
4005
+ /**
4006
+ * Serve the HLS playlist with rewritten segment URLs.
4007
+ */
4008
+ private servePlaylist;
4009
+ /**
4010
+ * Serve an HLS segment.
4011
+ */
4012
+ private serveSegment;
4013
+ /**
4014
+ * Cleanup expired sessions.
4015
+ */
4016
+ private cleanupExpiredSessions;
4017
+ private stopOtherSessionsWithPrefix;
4018
+ }
4019
+
4020
+ /**
4021
+ * Options for HlsSessionManager constructor.
4022
+ */
4023
+ export declare interface HlsSessionManagerOptions {
4024
+ /** Logger instance */
4025
+ logger?: Logger_2;
4026
+ /** Session TTL in milliseconds (default: 5 minutes) */
4027
+ sessionTtlMs?: number;
4028
+ /** Cleanup interval in milliseconds (default: 30 seconds) */
4029
+ cleanupIntervalMs?: number;
4030
+ }
4031
+
4032
+ /**
4033
+ * Parameters for creating a new HLS session.
4034
+ */
4035
+ export declare interface HlsSessionParams {
4036
+ /** Channel number */
4037
+ channel: number;
4038
+ /** Recording file name/path */
4039
+ fileName: string;
4040
+ /** Whether this is an NVR recording */
4041
+ isNvr?: boolean;
4042
+ /** External device ID for dedicated socket */
4043
+ deviceId?: string;
4044
+ /** Transcode H.265 to H.264 */
4045
+ transcodeH265ToH264?: boolean;
4046
+ /** HLS segment duration in seconds */
4047
+ hlsSegmentDuration?: number;
4048
+ }
4049
+
3805
4050
  /**
3806
4051
  * Intercom - Two-way audio support for Reolink cameras via Baichuan protocol.
3807
4052
  *
@@ -4165,6 +4410,8 @@ export declare interface ParsedRecordingFileName {
4165
4410
  durationMs: number;
4166
4411
  /** Frame rate extracted from filename hex flags (if available) */
4167
4412
  framerate?: number;
4413
+ /** File size in bytes extracted from filename (last hex field before extension) */
4414
+ sizeBytes?: number;
4168
4415
  flags?: RecordingVodFlags;
4169
4416
  rawFlags?: Record<string, number>;
4170
4417
  animalTypeRaw?: string;
@@ -4389,6 +4636,8 @@ export declare class ReolinkBaichuanApi {
4389
4636
  * Value: client, refCount, createdAt
4390
4637
  */
4391
4638
  private readonly dedicatedClients;
4639
+ /** Keep replay dedicated sockets warm briefly to reduce clip switch latency. */
4640
+ private static readonly REPLAY_DEDICATED_KEEPALIVE_MS;
4392
4641
  /**
4393
4642
  * Get a summary of currently active dedicated sessions.
4394
4643
  * Useful for debugging/logging to see how many sockets are open.
@@ -4510,6 +4759,17 @@ export declare class ReolinkBaichuanApi {
4510
4759
  * This ensures clean teardown at the end of each clip.
4511
4760
  */
4512
4761
  private releaseDedicatedClient;
4762
+ /**
4763
+ * Force-close a dedicated client if it exists.
4764
+ * This is called BEFORE entering the queue to immediately terminate any existing stream
4765
+ * for the same sessionKey. The existing stream will receive an error, release its queue slot,
4766
+ * and the new request can then proceed.
4767
+ *
4768
+ * @param sessionKey - The session key to force-close (e.g., `replay:${deviceId}`)
4769
+ * @param logger - Optional logger
4770
+ * @returns true if a client was closed, false if no client existed
4771
+ */
4772
+ private forceCloseDedicatedClient;
4513
4773
  /**
4514
4774
  * Create a dedicated Baichuan client session for streaming.
4515
4775
  * This is useful for consumers that need isolated socket connections per stream.
@@ -5751,11 +6011,13 @@ export declare class ReolinkBaichuanApi {
5751
6011
  * @param settings - Floodlight settings to apply
5752
6012
  *
5753
6013
  * @example
6014
+ * ```typescript
5754
6015
  * await api.setFloodlightSettings(0, {
5755
6016
  * duration: 300, // 5 minutes
5756
6017
  * detectType: 'people,vehicle',
5757
6018
  * brightness: 80,
5758
6019
  * });
6020
+ * ```
5759
6021
  */
5760
6022
  setFloodlightSettings(channel: number | undefined, settings: {
5761
6023
  duration?: number;
@@ -6365,6 +6627,13 @@ export declare class ReolinkBaichuanApi {
6365
6627
  * Default: false (passthrough/copy).
6366
6628
  */
6367
6629
  transcodeH265ToH264?: boolean;
6630
+ /**
6631
+ * Use MPEG-TS muxer to preserve frame timestamps (PTS).
6632
+ * When true, frames are muxed into MPEG-TS before being passed to ffmpeg.
6633
+ * This can help with variable framerate streams but may cause issues with some decoders.
6634
+ * Default: true (MPEG-TS muxing for proper timestamp alignment).
6635
+ */
6636
+ useMpegTsMuxer?: boolean;
6368
6637
  }): Promise<{
6369
6638
  mp4: Readable;
6370
6639
  stop: () => Promise<void>;
@@ -6408,6 +6677,98 @@ export declare class ReolinkBaichuanApi {
6408
6677
  mp4: Readable;
6409
6678
  stop: () => Promise<void>;
6410
6679
  }>;
6680
+ /**
6681
+ * Create an HLS (HTTP Live Streaming) session for a recording.
6682
+ *
6683
+ * This method creates HLS segments on-the-fly from a recording replay stream.
6684
+ * HLS is required for iOS devices (Safari, Home app) which don't support
6685
+ * fragmented MP4 streaming well and require Range request support.
6686
+ *
6687
+ * The session writes HLS segments (.ts files) and playlist (.m3u8) to a
6688
+ * temporary directory. You must serve these files via HTTP to the client.
6689
+ *
6690
+ * @example
6691
+ * ```ts
6692
+ * const session = await api.createRecordingReplayHlsSession({
6693
+ * channel: 0,
6694
+ * fileName: "/mnt/sda/Mp4Record/2026-01-25/RecS03.mp4",
6695
+ * });
6696
+ *
6697
+ * // Serve playlist
6698
+ * app.get('/clip.m3u8', (req, res) => {
6699
+ * res.type('application/vnd.apple.mpegurl');
6700
+ * res.send(session.getPlaylist());
6701
+ * });
6702
+ *
6703
+ * // Serve segments
6704
+ * app.get('/segment/:name', (req, res) => {
6705
+ * const data = session.getSegment(req.params.name);
6706
+ * if (data) {
6707
+ * res.type('video/mp2t');
6708
+ * res.send(data);
6709
+ * } else {
6710
+ * res.status(404).end();
6711
+ * }
6712
+ * });
6713
+ *
6714
+ * // Cleanup when done
6715
+ * await session.stop();
6716
+ * ```
6717
+ */
6718
+ createRecordingReplayHlsSession(params: {
6719
+ /** Channel number (0-based). Required. */
6720
+ channel: number;
6721
+ /** Full path to the recording file. Required. */
6722
+ fileName: string;
6723
+ /**
6724
+ * Force NVR mode (uses id-based XML with UID) or standalone mode (name-based XML).
6725
+ * If not specified, the library will detect based on device channel count.
6726
+ */
6727
+ isNvr?: boolean;
6728
+ /** Optional logger override. If not provided, uses the API's logger. */
6729
+ logger?: Logger;
6730
+ /**
6731
+ * External identifier for the dedicated socket session.
6732
+ * When provided, a dedicated BaichuanClient is created/reused for this deviceId.
6733
+ */
6734
+ deviceId?: string;
6735
+ /**
6736
+ * Transcode H.265/HEVC to H.264/AVC for compatibility.
6737
+ * Default: false (passthrough).
6738
+ */
6739
+ transcodeH265ToH264?: boolean;
6740
+ /**
6741
+ * HLS segment duration in seconds. Default: 4.
6742
+ */
6743
+ hlsSegmentDuration?: number;
6744
+ }): Promise<{
6745
+ /**
6746
+ * Get the current HLS playlist content (.m3u8).
6747
+ * Call this to serve the playlist to the client.
6748
+ */
6749
+ getPlaylist: () => string;
6750
+ /**
6751
+ * Get a segment file by name.
6752
+ * Returns undefined if the segment doesn't exist yet.
6753
+ */
6754
+ getSegment: (name: string) => Buffer | undefined;
6755
+ /**
6756
+ * List all available segment names.
6757
+ */
6758
+ listSegments: () => string[];
6759
+ /**
6760
+ * Wait for the HLS session to be ready (at least one segment available).
6761
+ */
6762
+ waitForReady: () => Promise<void>;
6763
+ /**
6764
+ * Stop the HLS session and cleanup.
6765
+ */
6766
+ stop: () => Promise<void>;
6767
+ /**
6768
+ * Path to the temporary directory containing HLS files.
6769
+ */
6770
+ tempDir: string;
6771
+ }>;
6411
6772
  /**
6412
6773
  * List recordings from a standalone camera.
6413
6774
  *
@@ -8013,7 +8374,8 @@ export declare type VodFile = {
8013
8374
  sec: number;
8014
8375
  };
8015
8376
  name: string;
8016
- size: number;
8377
+ /** File size in bytes - API may return as string or number */
8378
+ size: number | string;
8017
8379
  };
8018
8380
 
8019
8381
  export declare type VodSearchResponse = ReolinkCmdResponseExt<VodSearchResult> & {