Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add MJPEG support #88

Merged
merged 25 commits into from
Apr 21, 2024
Merged

Add MJPEG support #88

merged 25 commits into from
Apr 21, 2024

Conversation

zanshi
Copy link
Contributor

@zanshi zanshi commented Sep 22, 2023

Hi!

I've added MJPEG support to the crate based on RFC 2435.

It didn't fit in perfectly with the existing architecture (would maybe be nicer to add a new ImageParameters struct), but I think it's good enough?

I've verified that it works on two camera models from FlexWatch and Parantek. It also works with the GStreamer RTSP server.

I fuzzed it over the night and it didn't find anything.

Appreciate feedback and ideas for improvements!

Copy link
Owner

@scottlamb scottlamb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apologies for taking so long to get back to you on this. It looks really nice! I have some minor comments below.

It didn't fit in perfectly with the existing architecture (would maybe be nicer to add a new ImageParameters struct), but I think it's good enough?

How would ImageParameters differ from VideoParameters? We can do an API revision if necessary, but fwiw I don't see anything too wrong with using VideoParameters.


use super::{VideoFrame, VideoParameters};

const MAX_FRAME_LEN: usize = 2_000_000;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm guessing this is something you picked, rather than specified in the standard? Probably fine for now, but I wonder if we should have a mechanism to plumb things like this in.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I wanted some extra protection against corrupted bitstreams and just picked a large number.

}

precision = 0;
} else if qtable.is_none() {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems redundant; isn't it covered by the check on line 448? Also, it seems like length/qtable/precision are only relevant in the frag_offset == 0 case; might be a bit more elegant to combine the two if blocks describing that (361 and 403) into one that produces a non-Option qtable before doing the lines 423-445 stuff.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! Cleaned it up a bit

if length > 0 {
qtable = Some(payload.clone());
} else {
qtable = self.qtables[q as usize].clone();
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this check the q=255 special case? RFC 2435 section 3.1.8 says:

A Q value of 255 denotes that the quantization table mapping is dynamic and can change on every frame. Decoders MUST NOT depend on any previous version of the tables, and need to reload these tables on every frame. Packets MUST NOT contain Q = 255 and Length = 0.

timestamp,
parameters: Some(VideoParameters {
pixel_dimensions: (width as u32, height as u32),
rfc6381_codec: "".to_string(), // RFC 6381 is not applicable to MJPEG
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could switch this to an Option; I don't think a version bump for an API change would be a big deal.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you like me to make that change in this PR?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you like me to make that change in this PR?

Eh, I think it's pretty harmless to have the blank string too, so I'll just make a note to do it next time we have a version bump.

src/codec/jpeg.rs Show resolved Hide resolved
@zanshi
Copy link
Contributor Author

zanshi commented Dec 8, 2023

Sorry for the late reply as well! Apparently my github notifications were turned off. I've made a few commits to fix the issues you pointed out.

@zanshi
Copy link
Contributor Author

zanshi commented Dec 8, 2023

Apologies for taking so long to get back to you on this. It looks really nice! I have some minor comments below.

It didn't fit in perfectly with the existing architecture (would maybe be nicer to add a new ImageParameters struct), but I think it's good enough?

How would ImageParameters differ from VideoParameters? We can do an API revision if necessary, but fwiw I don't see anything too wrong with using VideoParameters.

Good point. It would probably just make the API more complicated. The idea to change rfc6381_codec to an Option sounds like a good idea.

@scottlamb
Copy link
Owner

What would you say to extending the example client to exercise this in some fashion—like writing a sequence of .jpg files? As I mentioned before, this all looks like very high-quality work. I'd like to just be able to give it a spin for myself, in part to way to check in following Retina changes if I've accidentally broken it...

@zanshi
Copy link
Contributor Author

zanshi commented Jan 5, 2024

That sounds like a good idea. I'll try to get time to do that soon!

@zanshi
Copy link
Contributor Author

zanshi commented Jan 8, 2024

I've added a test and an example. The example works pretty much like the mp4 one except that you specify an output directory to the JPEGs.

@scottlamb
Copy link
Owner

Finally getting through all the open PRs. Sorry yours has been waiting so long. Very nice work.

I've added a test and an example. The example works pretty much like the mp4 one except that you specify an output directory to the JPEGs.

Thank you! I don't think any of my cameras do MJPEG so I did a bit of hunting to come up with something to test against. I ended up with this Rube Goldberg machine:

  1. run mediamtx
  2. run ./client-record-format-mjpeg from gortsplib to copy packets from udp port 8000 to
  3. run gst-launch-1.0 videotestsrc ! video/x-raw,width=1920,height=1080,format=I420 ! jpegenc ! rtpjpegpay ! udpsink host=127.0.0.1 port=9000

...and then found gstreamer's rtspclientsink to simplify:

  1. run mediamtx
  2. run gst-launch-1.0 videotestsrc ! video/x-raw,width=1920,height=1080,format=I420 ! jpegenc ! rtspclientsink location=rtsp://localhost:8554/mystream

In the process I discovered what seems to be code for muxing jpegs to .mp4 format, in mediamtx's internal/playback/mp4/track.go. Might be interesting to make the mp4 example do this sometime.

@scottlamb scottlamb merged commit cd5d25b into scottlamb:main Apr 21, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants