Skip to content

Commit 38cc05c

Browse files
skrashevichAlexxIT
andauthored
feat(jpeg): Add keyframe caching with expiration mechanism to JPEG http handler (#1155)
* feat(mjpeg): add keyframe caching with expiration and cleanup goroutine * mjpeg: make keyframe cache duration and default usage configurable * mjpeg: document and add config options for MJPEG snapshot caching * mjpeg: fix errors after rebase * Code refactoring for frame.jpeg cache #1155 --------- Co-authored-by: Alex X <[email protected]>
1 parent 8c45771 commit 38cc05c

File tree

3 files changed

+92
-31
lines changed

3 files changed

+92
-31
lines changed

README.md

Lines changed: 4 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1236,35 +1236,11 @@ Read more about [codecs filters](#codecs-filters).
12361236

12371237
## Module: MJPEG
12381238

1239-
**Important.** For stream in MJPEG format, your source MUST contain the MJPEG codec. If your stream has an MJPEG codec, you can receive **MJPEG stream** or **JPEG snapshots** via API.
1239+
- This module can provide and receive streams in MJPEG format.
1240+
- This module is also responsible for receiving snapshots in JPEG format.
1241+
- This module also supports streaming to the server console (terminal) in the **animated ASCII art** format.
12401242

1241-
You can receive an MJPEG stream in several ways:
1242-
1243-
- some cameras support MJPEG codec inside [RTSP stream](#source-rtsp) (ex. second stream for Dahua cameras)
1244-
- some cameras have an HTTP link with [MJPEG stream](#source-http)
1245-
- some cameras have an HTTP link with snapshots - go2rtc can convert them to [MJPEG stream](#source-http)
1246-
- you can convert H264/H265 stream from your camera via [FFmpeg integraion](#source-ffmpeg)
1247-
1248-
With this example, your stream will have both H264 and MJPEG codecs:
1249-
1250-
```yaml
1251-
streams:
1252-
camera1:
1253-
- rtsp://rtsp:[email protected]/av_stream/ch0
1254-
- ffmpeg:camera1#video=mjpeg
1255-
```
1256-
1257-
API examples:
1258-
1259-
- MJPEG stream: `http://192.168.1.123:1984/api/stream.mjpeg?src=camera1`
1260-
- JPEG snapshots: `http://192.168.1.123:1984/api/frame.jpeg?src=camera1`
1261-
- You can use `width`/`w` and/or `height`/`h` params
1262-
- You can use `rotate` param with `90`, `180`, `270` or `-90` values
1263-
- You can use `hardware`/`hw` param [read more](https://github.com/AlexxIT/go2rtc/wiki/Hardware-acceleration)
1264-
1265-
**PS.** This module also supports streaming to the server console (terminal) in the **animated ASCII art** format ([read more](internal/mjpeg/README.md)).
1266-
1267-
[![](https://img.youtube.com/vi/sHj_3h_sX7M/mqdefault.jpg)](https://www.youtube.com/watch?v=sHj_3h_sX7M)
1243+
*[read more](internal/mjpeg/README.md)*
12681244

12691245
## Module: Log
12701246

internal/mjpeg/README.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,45 @@
1+
# MJPEG
2+
3+
**Important.** For stream in MJPEG format, your source MUST contain the MJPEG codec. If your stream has an MJPEG codec, you can receive **MJPEG stream** or **JPEG snapshots** via API.
4+
5+
You can receive an MJPEG stream in several ways:
6+
7+
- some cameras support MJPEG codec inside [RTSP stream](#source-rtsp) (ex. second stream for Dahua cameras)
8+
- some cameras have an HTTP link with [MJPEG stream](#source-http)
9+
- some cameras have an HTTP link with snapshots - go2rtc can convert them to [MJPEG stream](#source-http)
10+
- you can convert H264/H265 stream from your camera via [FFmpeg integraion](#source-ffmpeg)
11+
12+
With this example, your stream will have both H264 and MJPEG codecs:
13+
14+
```yaml
15+
streams:
16+
camera1:
17+
- rtsp://rtsp:[email protected]/av_stream/ch0
18+
- ffmpeg:camera1#video=mjpeg
19+
```
20+
21+
## API examples
22+
23+
**MJPEG stream**
24+
25+
```
26+
http://192.168.1.123:1984/api/stream.mjpeg?src=camera1`
27+
```
28+
29+
**JPEG snapshots**
30+
31+
```
32+
http://192.168.1.123:1984/api/frame.jpeg?src=camera1
33+
```
34+
35+
- You can use `width`/`w` and/or `height`/`h` params.
36+
- You can use `rotate` param with `90`, `180`, `270` or `-90` values.
37+
- You can use `hardware`/`hw` param [read more](https://github.com/AlexxIT/go2rtc/wiki/Hardware-acceleration).
38+
- You can use `cache` param (`1m`, `10s`, etc.) to get a cached snapshot.
39+
- The snapshot is cached only when requested with the `cache` parameter.
40+
- A cached snapshot will be used if its time is not older than the time specified in the `cache` parameter.
41+
- The `cache` parameter does not check the image sizes from the cache and those specified in the query.
42+
143
## Stream as ASCII to Terminal
244

345
[![](https://img.youtube.com/vi/sHj_3h_sX7M/mqdefault.jpg)](https://www.youtube.com/watch?v=sHj_3h_sX7M)

internal/mjpeg/init.go

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"net/http"
77
"strconv"
88
"strings"
9+
"sync"
910
"time"
1011

1112
"github.com/AlexxIT/go2rtc/internal/api"
@@ -36,12 +37,41 @@ func Init() {
3637
var log zerolog.Logger
3738

3839
func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
39-
stream, _ := streams.GetOrPatch(r.URL.Query())
40+
query := r.URL.Query()
41+
stream, _ := streams.GetOrPatch(query)
4042
if stream == nil {
4143
http.Error(w, api.StreamNotFound, http.StatusNotFound)
4244
return
4345
}
4446

47+
var b []byte
48+
49+
if s := query.Get("cache"); s != "" {
50+
if timeout, err := time.ParseDuration(s); err == nil {
51+
src := query.Get("src")
52+
53+
cacheMu.Lock()
54+
entry, found := cache[src]
55+
cacheMu.Unlock()
56+
57+
if found && time.Since(entry.timestamp) < timeout {
58+
writeJPEGResponse(w, entry.payload)
59+
return
60+
}
61+
62+
defer func() {
63+
entry = cacheEntry{payload: b, timestamp: time.Now()}
64+
cacheMu.Lock()
65+
if cache == nil {
66+
cache = map[string]cacheEntry{src: entry}
67+
} else {
68+
cache[src] = entry
69+
}
70+
cacheMu.Unlock()
71+
}()
72+
}
73+
}
74+
4575
cons := magic.NewKeyframe()
4676
cons.WithRequest(r)
4777

@@ -52,15 +82,15 @@ func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
5282

5383
once := &core.OnceBuffer{} // init and first frame
5484
_, _ = cons.WriteTo(once)
55-
b := once.Buffer()
85+
b = once.Buffer()
5686

5787
stream.RemoveConsumer(cons)
5888

5989
switch cons.CodecName() {
6090
case core.CodecH264, core.CodecH265:
6191
ts := time.Now()
6292
var err error
63-
if b, err = ffmpeg.JPEGWithQuery(b, r.URL.Query()); err != nil {
93+
if b, err = ffmpeg.JPEGWithQuery(b, query); err != nil {
6494
http.Error(w, err.Error(), http.StatusInternalServerError)
6595
return
6696
}
@@ -69,6 +99,19 @@ func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
6999
b = mjpeg.FixJPEG(b)
70100
}
71101

102+
writeJPEGResponse(w, b)
103+
}
104+
105+
var cache map[string]cacheEntry
106+
var cacheMu sync.Mutex
107+
108+
// cacheEntry represents a cached keyframe with its timestamp
109+
type cacheEntry struct {
110+
payload []byte
111+
timestamp time.Time
112+
}
113+
114+
func writeJPEGResponse(w http.ResponseWriter, b []byte) {
72115
h := w.Header()
73116
h.Set("Content-Type", "image/jpeg")
74117
h.Set("Content-Length", strconv.Itoa(len(b)))

0 commit comments

Comments
 (0)