Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Next Release
Add SMPTE336M and STANAG 4609 KLV parsing and writing to a raw .klv data file. Testing by passing MPEGTS with KLV data into a recent version of MediaMTX that outputs KLV over RTSP

# 3.0.8 - 15th January 2027
Add caching Digest Authentication code from Leone25 Enrico
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Yellowstone is co-developed by Michael Bullington and Roger Hardiman.
- ONVIF Metadata parsing (and writing to an output file)
- ONVIF extensions to RTSP
- ONVIF Audio Backchannel, sending ALaw audio to an IP Camera
- SMPTE336M KLV (STANAG 4609) KLV parsing (and writing to a .klv file)
- Simple RTCP parsing

## Examples
Expand Down
11 changes: 11 additions & 0 deletions dist/RTSPClient.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion dist/RTSPClient.js.map

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion dist/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import H265Transport from "./transports/H265Transport";
import H266Transport from "./transports/H266Transport";
import AV1Transport from "./transports/AV1Transport";
import AACTransport from "./transports/AACTransport";
import SMPTE336MKLVTransport from "./transports/SMPTE336MKLVTransport";
import ONVIFMetadataTransport from "./transports/ONVIFMetadataTransport";
import ONVIFClient from "./ONVIFClient";
import RTSPClient from "./RTSPClient";
import { RTPPacket, RTCPPacket } from "./util";
export { H264Transport, H265Transport, H266Transport, AV1Transport, AACTransport, ONVIFMetadataTransport, ONVIFClient, RTSPClient, RTPPacket, RTCPPacket };
export { H264Transport, H265Transport, H266Transport, AV1Transport, AACTransport, SMPTE336MKLVTransport, ONVIFMetadataTransport, ONVIFClient, RTSPClient, RTPPacket, RTCPPacket };
4 changes: 3 additions & 1 deletion dist/index.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion dist/index.js.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

33 changes: 23 additions & 10 deletions dist/transports/H264Transport.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion dist/transports/H264Transport.js.map

Large diffs are not rendered by default.

20 changes: 20 additions & 0 deletions dist/transports/SMPTE336MKLVTransport.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/// <reference types="node" />
/// <reference types="node" />
import RTSPClient from "../RTSPClient";
import { RTPPacket } from "../util";
import * as transform from "sdp-transform";
import { Writable } from "stream";
interface Details {
codec: string;
mediaSource: transform.MediaDescription;
rtpChannel: number;
rtcpChannel: number;
}
export default class SMPTE336MKLVTransport {
client: RTSPClient;
stream: Writable;
rawData: Buffer[];
constructor(client: RTSPClient, stream: Writable, details: Details);
processRTPPacket(packet: RTPPacket): void;
}
export {};
34 changes: 34 additions & 0 deletions dist/transports/SMPTE336MKLVTransport.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions dist/transports/SMPTE336MKLVTransport.js.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 7 additions & 1 deletion examples/demo.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

// Used to connect to Wowza Demo URL but they have taken it away, and the replacement URL on their web site does not work.

const { RTSPClient, H264Transport, H265Transport, H266Transport, AV1Transport, AACTransport } = require("../dist");
const { RTSPClient, H264Transport, H265Transport, H266Transport, AV1Transport, AACTransport, SMPTE336MKLVTransport } = require("../dist");
const fs = require("fs");
const { exit } = require("process");
const { program } = require("commander");
Expand Down Expand Up @@ -96,6 +96,12 @@ client.connect(url, { connection: transport, secure: false })
// This class subscribes to the client 'data' event, looking for the audio payload
const aac = new AACTransport(client, audioFile, details);
}
if (details.codec == "SMPTE336M") {
const klvFile = fs.createWriteStream(filename + '.klv');
// Add KLV Transport
// This class subscribes to the client 'data' event, looking for the KLV payload
const klv = new SMPTE336MKLVTransport(client, klvFile, details);
}
}

// Step 5: Start streaming!
Expand Down
13 changes: 13 additions & 0 deletions lib/RTSPClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,19 @@ export default class RTSPClient extends EventEmitter {
}
}

if (
mediaSource.type === "application" &&
mediaSource.protocol === RTP_AVP &&
mediaSource.rtp[0].codec.toUpperCase() === "SMPTE336M" // MediaMTX sends in capitals. Looks like the RFC suggests lower case
) {
this.emit("log", "SMPTE336M KLV Data Stream Found in SDP", "");
if (hasMetaData == false) {
needSetup = true;
hasMetaData = true;
codec = "SMPTE336M";
}
}

if (needSetup) {
let streamurl = "";
// The 'control' in the SDP can be a relative or absolute uri
Expand Down
2 changes: 2 additions & 0 deletions lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import H265Transport from "./transports/H265Transport";
import H266Transport from "./transports/H266Transport";
import AV1Transport from "./transports/AV1Transport";
import AACTransport from "./transports/AACTransport";
import SMPTE336MKLVTransport from "./transports/SMPTE336MKLVTransport";
import ONVIFMetadataTransport from "./transports/ONVIFMetadataTransport";
import ONVIFClient from "./ONVIFClient";
import RTSPClient from "./RTSPClient";
Expand All @@ -14,6 +15,7 @@ export {
H266Transport,
AV1Transport,
AACTransport,
SMPTE336MKLVTransport,
ONVIFMetadataTransport,
ONVIFClient,
RTSPClient,
Expand Down
40 changes: 28 additions & 12 deletions lib/transports/H264Transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,19 +49,35 @@ export default class H264Transport {
return;
}

// Normally the SDP includes the sprop-parameter-sets with the SPS and PPS data
// However for MPEGTS converted to RTSP with MediaMTX, there is no sprop-parameter-sets
const fmtpConfig = transform.parseParams(fmtp.config);
const splitSpropParameterSets = fmtpConfig['sprop-parameter-sets'].toString().split(',');
const sps_base64 = splitSpropParameterSets[0];
const pps_base64 = splitSpropParameterSets[1];
const sps = Buffer.from(sps_base64, "base64");
const pps = Buffer.from(pps_base64, "base64");

this.stream.write(H264_HEADER);
this.stream.write(sps);
this.stream.write(H264_HEADER);
this.stream.write(pps);

this._headerWritten = true;
if ('sprop-parameter-sets' in fmtpConfig) {
const splitSpropParameterSets = fmtpConfig['sprop-parameter-sets'].toString().split(',');
const sps_base64 = splitSpropParameterSets[0];
const pps_base64 = splitSpropParameterSets[1];
const sps = Buffer.from(sps_base64, "base64");
const pps = Buffer.from(pps_base64, "base64");

this.stream.write(H264_HEADER);
this.stream.write(sps);
this.stream.write(H264_HEADER);
this.stream.write(pps);

this._headerWritten = true;
}
else
{
// Ideally MediaMTX would have parsed the MPEGTS stream, extracted the SPS and PPS and then
// placed it in the RTSP DESCRIBE SDP, but it does not do that.
// The correct method is to parse the RTP Payloads until we see the NAL type for SPS
// and the NAL type for PPS, then we can write them and set this._headerWritten to true

// But for now we will just set this._headerWritten to true and let NALS be written to disk
// before the first SPS and PPS data
this._headerWritten = true; // HACK - should be parsing the NALs for SPS and PPS
}

}

processRTPPacket(packet: RTPPacket): void {
Expand Down
52 changes: 52 additions & 0 deletions lib/transports/SMPTE336MKLVTransport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// De-packetize RC 6597 RTP packets to re-create SMPTE336M KLV Metadata including STANAG 4609
// Write data to a file as raw binary data.
// The RTP timestamp is not saved to the file.
// By Roger Hardiman, January 2026

import RTSPClient from "../RTSPClient";
import { RTPPacket } from "../util";

import * as transform from "sdp-transform";
import { Writable } from "stream";

interface Details {
codec: string;
mediaSource: transform.MediaDescription;
rtpChannel: number;
rtcpChannel: number;
}

export default class SMPTE336MKLVTransport {
client: RTSPClient;
stream: Writable;
rawData: Buffer[];

constructor(client: RTSPClient, stream: Writable, details: Details) {
this.client = client;
this.stream = stream;
this.rawData = [];

client.on("data", (channel, data, packet) => {
if (channel == details.rtpChannel) {
this.processRTPPacket(packet);
}
});
}

processRTPPacket(packet: RTPPacket): void {
// RTP Payload for ONVIF Metadata

// Accumulate payload
this.rawData.push(packet.payload)

if (packet.marker == 1) { // TODO... OR if the Timestamp has changed
// end of data. Write the file
// In this case we can just write each Buffer from the rawData array
// If we were passing the KLV to a caller, we would concatenate the Buffers in the rawData array first
for(const buffer of this.rawData) {
this.stream.write(buffer);
}
this.rawData = [];
}
}
}
Loading