Post-processing tools
Janus records calls to its own .mjr format (Meetecho JanusRecording) and can dump RTP to pcap files. Four small command line tools convert between these formats and play files that other software understands. A fifth, text2pcap, is not a tool here: see the end of the page.
| Tool | Does |
|---|---|
janus-pp-rec | .mjr to a normal media file: .opus, .ogg, .mka, .wav, .webm, .mkv, .mp4, .srt. Also reads and checks .mjr files. |
mjr2pcap | .mjr to .pcap, so Wireshark can open it. |
pcap2mjr | .pcap back to .mjr, for a capture you want to convert with janus-pp-rec. |
janus-cfgconv | Janus config files between the old .cfg (INI) and the current .jcfg (libconfig) formats. |
text2pcap | Not a tool. The server writes text or pcap dumps through two Admin API requests. |
Build them
The tools are crates in the workspace. Build the four of them with Cargo; each program is named after its crate.
cargo build --release -p janus-pp-rec -p mjr2pcap -p pcap2mjr -p janus-cfgconv
ls target/release/janus-pp-rec target/release/mjr2pcap target/release/pcap2mjr target/release/janus-cfgconvThe transcripts on this page were run from a debug build in a scratch folder. The tools print a banner with the Janus version and commit, which the transcripts leave out. janus-pp-rec and pcap2mjr log in colour unless you give -o (pp-rec) or send the output to a file.
All four are ports of the C tools at the pinned oracle commit, checked in the differential harness against the C programs: janus-pp-rec over 397 scenarios and about 5000 random recordings, mjr2pcap over 1692 cases, pcap2mjr over 5075, and janus-cfgconv over 3626 (parity/status.json). The C tools came from the oracle Docker image; I did not run them here.
Where .mjr files come from
Plugins record when you ask them to: EchoTest with record: true and a filename, and VideoRoom, AudioBridge and RecordPlay with their own options (see the plugin pages). Each medium gets its own file; EchoTest names it after the filename you give plus -audio or -video. protected_folders in janus.jcfg lists folders where Janus must not create files.
The examples use two recordings made by the server. An EchoTest call from headless Chrome with fake devices asked for record: true and filename: ".../echo", so Janus wrote echo-audio.mjr (Opus) and echo-video.mjr (VP8). The file crates/janus-record/tests/fixtures/rec-sample-audio.mjr in the repository is a short Opus recording in the old header format that you can try without a server.
janus-pp-rec
janus-pp-rec [OPTION...] source.mjr [destination.<ext>]The destination's extension picks the output container. -F lists which extensions each codec supports.
| Option | Meaning |
|---|---|
-F, --file-extensions | Print the supported target extensions per codec and exit. |
-j, --json | Print only the JSON header of the recording and exit. |
-H, --header | Only parse the header, with log output. |
-p, --parse | Only parse the file and re-order the packets. Writes nothing. |
-e, --extended-json | Print an extended JSON report (also sets -j): the header plus extended with the packet count and duration, and for video the resolution (and rotation changes, if any). |
-m, --metadata | A metadata string to store in the target file. |
-i, --ignore-first | Skip the first N packets (when they are broken). Default 0. |
-P, --payload-type | Ignore packets that do not have this payload type. |
-a, --audiolevel-ext | The ID of the audio-level RTP extension, to use it. |
-v, --videoorient-ext | The ID of the video-orientation extension. |
-d, --debug-level | Log level, 0 (off) to 7. Default 4. |
-D, --debug-timestamps | Add timestamps to the log lines. |
-o, --disable-colors | No colours in the log. |
-f, --format | The output format, overriding the destination's extension. |
-t, --faststart | For .mp4, write the moov atom first. |
-S, --audioskew | If the audio is off by more than this many milliseconds, compensate. Off with 0 (the default). |
-C, --silence-distance | Packet distance that marks silence suppression. Off with 0. |
-r, --restamp | Correct timestamps when the latency exceeds the moving average times this ratio. Off with 0. |
-c, --restamp-packets | Packets used for the moving average of -r. Default 10. |
-n, --restamp-min-th | The smallest average latency before -r acts. Default 500. |
-I, --ignore-rtp-ts | Use packet arrival times instead of RTP timestamps. |
Outputs by codec
| Codec | Targets |
|---|---|
| Opus | .opus, .ogg, .mka |
| G.711, G.722, L16 | .wav |
| VP8, VP9 | .webm, .mkv |
| H.264, H.265 | .mp4, .mkv |
| AV1 | .mp4 |
| text data channel | .srt |
| any (binary data) | any |
Try it on the fixture
# The checked-in fixture crates/janus-record/tests/fixtures/rec-sample-audio.mjr is a short Opus recording. Copy it to the current folder first.
# Which output formats exist for which codec.
$ janus-pp-rec -F
Supported file extensions:
-- Opus: [opus, ogg, mka]
-- G.711: [wav]
-- G.722: [wav]
-- L16: [wav]
-- VP8: [webm, mkv]
-- VP9: [webm, mkv]
-- H.264: [mp4, mkv]
-- AV1: [mp4]
-- H.265: [mp4, mkv]
-- Text: [srt]
-- Binary: any
# Print the JSON header of the recording.
$ janus-pp-rec -j sample.mjr
[exit status 1]
# Parse only the header.
$ janus-pp-rec -H -o sample.mjr
Logging level: 4
Source file: sample.mjr
-- Showing header only
File is 53931 bytes
Pre-parsing file to generate ordered index...
[WARN] Old .mjr header format
This is an audio recording, assuming Opus
# Convert to an Ogg Opus file (-o turns the colours off).
$ janus-pp-rec -o sample.mjr sample.opus
Logging level: 4
Source file: sample.mjr
Target file: sample.opus
File is 53931 bytes
Pre-parsing file to generate ordered index...
[WARN] Old .mjr header format
This is an audio recording, assuming Opus
SSRC detected: 3649154666
Counted 529 RTP packets
Counted 529 frame packets
[WARN] Detected audio clock mismatch, consider using skew compensation or restamping (rtp_time=10.56s, real_time=0.00s, diff=10.56s)
sample.opus is 43233 bytes
Bye!
# Check the result with ffprobe (not part of Janus).
$ ffprobe -v error -show_entries stream=codec_name,sample_rate,channels:format=format_name,duration -of default=nw=1 sample.opus
codec_name=opus
sample_rate=48000
channels=2
format_name=ogg
duration=10.580000The old header format has no JSON header, so -j prints nothing for this file and exits with status 1, as the C tool does. The Detected audio clock mismatch warning is the tool noticing that the timestamps in this very old file do not match its arrival times. The other options help when it matters (-S, -r, -I).
Try it on a recording from the server
# Recordings made by the server: an EchoTest call from headless Chrome with record: true and filename: echo. Janus wrote echo-audio.mjr and echo-video.mjr.
$ ls -l echo-*.mjr
38589 echo-audio.mjr
496784 echo-video.mjr
# The JSON header: type, codec, creation time and time of the first packet, in microseconds.
$ janus-pp-rec -j echo-audio.mjr
{"t": "a", "c": "opus", "s": 1793808517141679, "u": 1793808518471308}
# The extended report: sizes, packet counts and timing, as JSON.
$ janus-pp-rec -e echo-audio.mjr
{"t":"a","c":"opus","s":1793808517141679,"u":1793808518471308,"extended":{"packets":408,"duration":8.1400000000000006}}
# The same for the video recording: the size of the picture is in the report.
$ janus-pp-rec -e echo-video.mjr
{"t":"v","c":"vp8","s":1793808517141855,"u":1793808518502179,"extended":{"packets":492,"duration":8.2100333333333335,"codec":{"resolution":[{"ts":0.0025222222222222221,"width":640,"height":480}]}}}
# Only parse and re-order the packets (no output file).
$ janus-pp-rec -p -o echo-audio.mjr
Logging level: 4
Source file: echo-audio.mjr
-- Parsing header only
File is 38589 bytes
Pre-parsing file to generate ordered index...
This is an audio recording:
-- Codec: opus
-- Created: 1793808517141679
-- Written: 1793808518471308
SSRC detected: 2472867687
Counted 408 RTP packets
Counted 408 frame packets
Parsing and reordering completed, bye!
# Audio to Opus.
$ janus-pp-rec -o echo-audio.mjr audio.opus
Logging level: 4
Source file: echo-audio.mjr
Target file: audio.opus
File is 38589 bytes
Pre-parsing file to generate ordered index...
This is an audio recording:
-- Codec: opus
-- Created: 1793808517141679
-- Written: 1793808518471308
SSRC detected: 2472867687
Counted 408 RTP packets
Counted 408 frame packets
audio.opus is 23871 bytes
Bye!
# Opus can also go to .ogg or .mka (Matroska).
$ janus-pp-rec -o echo-audio.mjr audio.ogg
audio.ogg is 23871 bytes
Bye!
$ janus-pp-rec -o echo-audio.mjr audio.mka
audio.mka is 26115 bytes
Bye!
# The target must suit the codec.
$ janus-pp-rec -o echo-audio.mjr audio.webm
Pre-parsing file to generate ordered index...
[ERR] [postprocessing/janus-pp-rec.c:main:602] Opus RTP packets cannot be converted to this target file, at the moment (supported formats: [opus, ogg, mka])
[exit status 1]
# A source file that does not exist.
$ janus-pp-rec -o missing.mjr x.opus
[ERR] [postprocessing/janus-pp-rec.c:main:357] Could not open file missing.mjr
[exit status 1]
# -f picks the container when the file name does not say it (VP8 goes to .webm or .mkv).
$ janus-pp-rec -o -f mkv echo-video.mjr video.mkv
video.mkv is 478099 bytes
Bye!
# VP8 video to WebM. The warning is C behaviour: the tool looks for the VP8 sync code in the first bytes of each keyframe and warns when it is not there.
$ janus-pp-rec -o echo-video.mjr video.webm
Logging level: 4
Source file: echo-video.mjr
Target file: video.webm
File is 496784 bytes
Pre-parsing file to generate ordered index...
This is a video recording:
-- Codec: vp8
-- Created: 1793808517141855
-- Written: 1793808518502179
SSRC detected: 1405753589
Counted 496 RTP packets
Counted 492 frame packets
[WARN] Dropping previously marked video packet (time ~0s)
[WARN] Lost a packet here? (got seq 8 after 4, time ~0s)
[WARN] Lost a packet here? (got seq 27 after 25, time ~0s)
-- 640x480 (fps [72,15059] ~ 1250)
First keyframe: 227
video.webm is 478036 bytes
Bye!
# Check with ffprobe (not part of Janus).
$ ffprobe audio.opus; ffprobe video.webm (abridged)
codec_name=opus
sample_rate=48000
channels=2
duration=8.160000
---
codec_name=vp8
width=640
height=480
duration=8.210000
# Janus writes audio and video to separate files. To get one file, mux them with ffmpeg (not part of Janus; no re-encoding).
$ ffmpeg -y -i video.webm -i audio.opus -c copy merged.webm && ffprobe merged.webm (abridged)
codec_name=vp8
codec_type=video
codec_name=opus
codec_type=audio
duration=8.256000What you see:
-jis the header:tisa,vord(audio, video, data);cthe codec;sthe time the recording was created anduwhen the first packet was written, both in microseconds since the epoch.-eadds"extended": {"packets": ..., "duration": ...}, and for video acodec.resolutionlist with the size and the time it applied from.Counted N RTP packetsandN frame packetsare what the parser found; a video recording has fewer frames than packets.- The VP8 warnings (
Late pre-reset packet,Lost a packet here?) come from the tool, which keeps the C wording. A video recording of a live call is rarely perfect. The result plays:ffprobeshows a 640x480 VP8 stream. - Audio and video go to separate files. Muxing them into one is
ffmpeg's job, as the last step shows.
mjr2pcap
mjr2pcap source.mjr destination.pcapWrites each RTP packet of the recording into a pcap file with made-up Ethernet, IPv4 and UDP headers, so Wireshark and tshark recognise RTP. The pcap time stamps come from the recording. There are no options.
# With no arguments the tool prints its usage.
$ mjr2pcap
Usage: mjr2pcap source.mjr destination.pcap
[exit status 1]
# A source file that does not exist.
$ mjr2pcap missing.mjr x.pcap
missing.mjr --> x.pcap
[ERR] [postprocessing/mjr2pcap.c:main:173] Could not open file missing.mjr
[exit status 1]
# An .mjr to a pcap. The packets get made-up Ethernet, IP and UDP headers; the RTP is what Janus recorded.
$ mjr2pcap echo-audio.mjr audio.pcap
echo-audio.mjr --> audio.pcap
File is 38589 bytes
Pre-parsing file...
Traversing RTP packets...
audio.pcap is 58118 bytes
Bye!
# The pcap file header (magic d4 c3 b2 a1, little endian, link type 1) and the first record.
$ ls -l audio.pcap && xxd -l 64 audio.pcap
58118 audio.pcap
00000000: d4c3 b2a1 0200 0400 0000 0000 0000 0000 ................
00000010: ffff 0000 0100 0000 8658 eb6a 0c31 0700 .........X.j.1..
00000020: 6600 0000 6600 0000 0000 0000 0000 0000 f...f...........
00000030: 0000 0000 0800 4500 0058 0000 4000 4011 ......E..X..@.@.pcap2mjr
pcap2mjr [OPTION...] source.pcap destination.mjr| Option | Meaning |
|---|---|
-c, --codec | The codec of the stream, for example opus, vp8, h264. Required. |
-s, --ssrc | The SSRC to take from the file. 0 (the default) detects the first RTP stream. |
-w, --warnings | Show a warning for each skipped packet (not RTP, or the wrong SSRC). |
The pcap must hold UDP packets with RTP, as mjr2pcap or Janus's own start_pcap write them. The codec is not in the pcap, so you say it. A capture with several streams needs -s, since the tool would take the first one it sees.
# Options.
$ pcap2mjr -h
Usage:
pcap2mjr [OPTION?] source.pcap destination.mjr
Help Options:
-h, --help Show help options
Application Options:
-c, --codec Codec the recording will contain (e.g., opus, vp8, etc.)
-s, --ssrc SSRC of the packets in the pcap file to save (pass 0 to autodetect)
-w, --warnings Show warnings for skipped packets (e.g., not RTP or wrong SSRC)
# The pcap made above, back to an .mjr. No SSRC given: the tool picks the first RTP stream it finds.
$ pcap2mjr -c opus audio.pcap back.mjr
[opus/0] audio.pcap --> back.mjr
[WARN] No SSRC provided, will try to autodetect an RTP stream
Autodetected SSRC 2472867687
Saved 408 out of 408 packets
back.mjr is 38589 bytes
Bye!
# The result is a valid recording.
$ janus-pp-rec -j back.mjr
{"t": "a", "c": "opus", "s": 1793808518471308, "u": 1793808518471308}
# Round trip: the packet count of the original recording...
$ janus-pp-rec -p -o echo-audio.mjr
Counted 408 RTP packets
Counted 408 frame packets
# ...and of the one that came out of pcap2mjr. The headers differ (pcap has no recording times, so the written time is the creation time), the packets do not.
$ janus-pp-rec -p -o back.mjr
Counted 408 RTP packets
Counted 408 frame packets
# Without -c the tool refuses.
$ pcap2mjr audio.pcap x.mjr
Usage:
pcap2mjr [OPTION?] source.pcap destination.mjr
Help Options:
-h, --help Show help options
Application Options:
-c, --codec Codec the recording will contain (e.g., opus, vp8, etc.)
-s, --ssrc SSRC of the packets in the pcap file to save (pass 0 to autodetect)
-w, --warnings Show warnings for skipped packets (e.g., not RTP or wrong SSRC)
[exit status 1]The recording that comes back has the same packets. Its header cannot be the same, because a pcap has no creation time: s and u both become the time of the first packet.
janus-cfgconv
janus-cfgconv source.[cfg|jcfg] destination.[cfg|jcfg]
janus-cfgconv --parse source.[cfg|jcfg]The Janus config format changed from .cfg (INI) to .jcfg (libconfig). The tool converts either way, by file extension, and prints both files as it read and wrote them. --parse reads one file and prints what it found, which is a quick way to check a hand-edited file.
# The legacy .cfg format (INI) to the current .jcfg format (libconfig). The input is this file:
$ cat janus.transport.http.cfg
[general]
json = indented
base_path = /janus
http = yes
port = 8088
[admin]
admin_http = no
[certificates]
; cert_pem = /path/to/cert.pem
$ janus-cfgconv janus.transport.http.cfg janus.transport.http.jcfg
Converting:
-- IN: janus.transport.http.cfg
-- OUT: janus.transport.http.jcfg
[janus.transport.http.cfg]
general: {
json: indented
base_path: /janus
http: yes
port: 8088
}
admin: {
admin_http: no
}
certificates: {
}
[janus.transport.http.jcfg]
general: {
json: indented
base_path: /janus
http: yes
port: 8088
}
admin: {
admin_http: no
}
certificates: {
}
janus.transport.http.jcfg is 214 bytes
Bye!
$ cat janus.transport.http.jcfg
#
# File automatically generated on Wed Nov 4 21:38:52 2026
#
general :
{
json = "indented";
base_path = "/janus";
http = "yes";
port = "8088";
};
admin :
{
admin_http = "no";
};
certificates :
{
};
# And back.
$ janus-cfgconv janus.transport.http.jcfg back.cfg
Converting:
-- IN: janus.transport.http.jcfg
-- OUT: back.cfg
[janus.transport.http.jcfg]
general: {
json: indented
base_path: /janus
http: yes
port: 8088
}
admin: {
admin_http: no
}
certificates: {
}
[back.cfg]
general: {
json: indented
base_path: /janus
http: yes
port: 8088
}
admin: {
admin_http: no
}
certificates: {
}
back.cfg is 177 bytes
Bye!
$ cat back.cfg
;
; File automatically generated on Wed Nov 4 21:38:52 2026
;
[general]
json = indented
base_path = /janus
http = yes
port = 8088
[admin]
admin_http = no
[certificates]
# --parse only checks that the file reads, and prints what it found.
$ janus-cfgconv --parse janus.transport.http.jcfg
Parsing:
-- IN: janus.transport.http.jcfg
[janus.transport.http.jcfg]
general: {
json: indented
base_path: /janus
http: yes
port: 8088
}
admin: {
admin_http: no
}
certificates: {
}
Bye!
# A file that does not parse.
$ janus-cfgconv --parse broken.jcfg
Parsing:
-- IN: broken.jcfg
[ERR] [config.c:janus_config_parse:205] Error parsing config file at line 3: syntax error
[exit status 1]Things to know, all visible above: the converter writes every value as a string (http = "yes", port = "8088"), which Janus reads fine (it treats yes, true and 1 alike) but is not how you would write the file by hand. Comments are dropped. A file that does not parse ends the run with an error and status 1. Janus itself reads .jcfg first and falls back to .cfg, so a converted file is optional.
text2pcap (no standalone tool)
text2pcap is not a command in this repo or in the C server. In the C code, src/text2pcap.c is a library: it has no main, no options and no usage text. The only way to use it is two Admin API requests that dump the packets of one handle while a call runs: start_pcap writes a pcap file, start_text2pcap writes a text file that Wireshark's own text2pcap program can turn into a pcap (the hex dump format: direction, time, offset, bytes). The Rust version is in crates/janus-record (dump.rs, dump_name.rs, pcap.rs), and the Admin API requests are in crates/janus-core/src/admin.rs.
| Request | Fields | Effect |
|---|---|---|
start_pcap | folder, filename, truncate (bytes per packet, optional) | Starts a pcap dump of the handle's RTP and RTCP, both directions. |
start_text2pcap | the same | Starts the text dump. |
stop_pcap, stop_text2pcap | none | Stops it. |
Send them to the Admin API on the handle path: POST /admin/<session>/<handle>. Errors: Capture not started (stop without start), pcap already started, and Error starting pcap dump (a folder that cannot be created, or a protected one), all error 490. The transcript is a real run during a call. It ends with the pipeline pcap to .mjr to .opus that uses the tools above. The text file was not converted: Wireshark is not installed here.
# Janus can write the RTP and RTCP of a handle to a file while a call runs: the Admin API requests start_pcap (a pcap file) and start_text2pcap (a text file in the format of Wireshark's text2pcap). There is no standalone text2pcap tool in this repo: the code is in the server (crates/janus-record), and these two Admin API requests are its only way in.
# The requests, as POST /admin/<session>/<handle> with admin_secret and transaction, and the replies (the transaction and secret are left out):
# stop with nothing running
> {"janus":"stop_pcap"}
< {"janus":"error","error":{"code":490,"reason":"Capture not started"}}
# folder that cannot be made
> {"janus":"start_pcap","folder":"/dev/null/x","filename":"call.pcap"}
< {"janus":"error","error":{"code":490,"reason":"Error starting pcap dump"}}
# start
> {"janus":"start_pcap","folder":"/var/folders/px/r2rzqr_n57x7ldc460wdpk540000gn/T/janus-docs-tools-zFu3VJ/dump","filename":"call.pcap"}
< {"janus":"success"}
# start again
> {"janus":"start_pcap","folder":"/var/folders/px/r2rzqr_n57x7ldc460wdpk540000gn/T/janus-docs-tools-zFu3VJ/dump","filename":"call.pcap"}
< {"janus":"error","error":{"code":490,"reason":"pcap already started"}}
# stop
> {"janus":"stop_pcap"}
< {"janus":"success"}
# start the text format
> {"janus":"start_text2pcap","folder":"/var/folders/px/r2rzqr_n57x7ldc460wdpk540000gn/T/janus-docs-tools-zFu3VJ/dump","filename":"call.txt"}
< {"janus":"success"}
# stop the text format
> {"janus":"stop_text2pcap"}
< {"janus":"success"}
# Janus wrote both files in the folder from the request (2.5 s of pcap, 0.8 s of text).
$ ls -l dump
315998 call.pcap
336280 call.txt
# The pcap holds audio, video and RTCP of both directions. Take one stream with -s (here the audio SSRC 2472867687, which the .mjr recording of the same call reports).
$ pcap2mjr -c opus -s 2472867687 dump/call.pcap call-audio.mjr
[opus/2472867687] dump/call.pcap --> call-audio.mjr
Saved 118 out of 565 packets
call-audio.mjr is 10475 bytes
Bye!
$ janus-pp-rec -o call-audio.mjr call-audio.opus
SSRC detected: 2472867687
Counted 118 RTP packets
Counted 118 frame packets
call-audio.opus is 6325 bytes
Bye!
# The text file: one line per packet, direction (I in, O out), time, offset, then the packet as hex bytes. RTCP lines end with the marker JANUS_TEXT2PCAP_RTCP and the session and handle ids. This is the format that Wireshark's text2pcap reads; Wireshark is not installed here, so that step was not run.
$ head -n 3 dump/call.txt | cut -c1-110
I 16:08:40.831016 000000 af cd 00 06 5b a6 3d dd 5f df 44 54 00 76 00 03 78 5e a8 1e 20 03 8a 00 00 00 00 03
I 16:08:40.833130 000000 90 6f 30 76 05 20 fe fb 93 64 f7 67 be de 00 03 22 22 bc 8c 31 01 02 10 ba 00 00 00
O 16:08:40.833186 000000 90 6f 00 77 00 01 ba 80 99 f5 3c 44 be de 00 01 10 ba 40 30 78 09 8a 78 3c a2 e8 d8The C comment at the top of text2pcap.c says new packets are appended to an existing file; the code opens it for writing and truncates it, and the port follows the code (PARITY_LOG.md, wave 103).
Known gaps
- gap
janus-pp-recwrites Opus as Ogg and Matroska, but G.722 and G.711.wav, H.264 and H.265.mp4, and AV1.mp4were not run here. They are covered by the differential harness against the C tool. H.264 and H.265 to.mkvare byte-exact with the C tool (wave 248);.mp4output uses a box writer written for the port and has not been checked against ffmpeg'slibavformat, which the C tool uses (parity/status.json: "playability" is open). - gap In a file whose only real header comes after a zero-length one,
janus-pp-recstill exits 1 (parity/status.json, wave 242). - gap I only ran
pcap2mjron classic pcap files. The component note inparity/status.jsonstill says "pcapng missing" next to a wave that checked pcapng cases, so I do not know which holds. - gap
janus-cfgconvdoes not echo one flex quirk of the C tool for an unterminated@include(parity/status.json).
Test it in Janus Bench
Janus Bench does not run these tools. Its EchoTest echotest.record check starts and stops a plugin recording, which is what produces the .mjr files these tools read.