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 <alexey.khit@gmail.com>
This commit is contained in:
committed by
GitHub
parent
8c457710bd
commit
38cc05c22d
@@ -1236,35 +1236,11 @@ Read more about [codecs filters](#codecs-filters).
|
|||||||
|
|
||||||
## Module: MJPEG
|
## Module: MJPEG
|
||||||
|
|
||||||
**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.
|
- This module can provide and receive streams in MJPEG format.
|
||||||
|
- This module is also responsible for receiving snapshots in JPEG format.
|
||||||
|
- This module also supports streaming to the server console (terminal) in the **animated ASCII art** format.
|
||||||
|
|
||||||
You can receive an MJPEG stream in several ways:
|
*[read more](internal/mjpeg/README.md)*
|
||||||
|
|
||||||
- some cameras support MJPEG codec inside [RTSP stream](#source-rtsp) (ex. second stream for Dahua cameras)
|
|
||||||
- some cameras have an HTTP link with [MJPEG stream](#source-http)
|
|
||||||
- some cameras have an HTTP link with snapshots - go2rtc can convert them to [MJPEG stream](#source-http)
|
|
||||||
- you can convert H264/H265 stream from your camera via [FFmpeg integraion](#source-ffmpeg)
|
|
||||||
|
|
||||||
With this example, your stream will have both H264 and MJPEG codecs:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
streams:
|
|
||||||
camera1:
|
|
||||||
- rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0
|
|
||||||
- ffmpeg:camera1#video=mjpeg
|
|
||||||
```
|
|
||||||
|
|
||||||
API examples:
|
|
||||||
|
|
||||||
- MJPEG stream: `http://192.168.1.123:1984/api/stream.mjpeg?src=camera1`
|
|
||||||
- JPEG snapshots: `http://192.168.1.123:1984/api/frame.jpeg?src=camera1`
|
|
||||||
- You can use `width`/`w` and/or `height`/`h` params
|
|
||||||
- You can use `rotate` param with `90`, `180`, `270` or `-90` values
|
|
||||||
- You can use `hardware`/`hw` param [read more](https://github.com/AlexxIT/go2rtc/wiki/Hardware-acceleration)
|
|
||||||
|
|
||||||
**PS.** This module also supports streaming to the server console (terminal) in the **animated ASCII art** format ([read more](internal/mjpeg/README.md)).
|
|
||||||
|
|
||||||
[](https://www.youtube.com/watch?v=sHj_3h_sX7M)
|
|
||||||
|
|
||||||
## Module: Log
|
## Module: Log
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,45 @@
|
|||||||
|
# MJPEG
|
||||||
|
|
||||||
|
**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.
|
||||||
|
|
||||||
|
You can receive an MJPEG stream in several ways:
|
||||||
|
|
||||||
|
- some cameras support MJPEG codec inside [RTSP stream](#source-rtsp) (ex. second stream for Dahua cameras)
|
||||||
|
- some cameras have an HTTP link with [MJPEG stream](#source-http)
|
||||||
|
- some cameras have an HTTP link with snapshots - go2rtc can convert them to [MJPEG stream](#source-http)
|
||||||
|
- you can convert H264/H265 stream from your camera via [FFmpeg integraion](#source-ffmpeg)
|
||||||
|
|
||||||
|
With this example, your stream will have both H264 and MJPEG codecs:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
streams:
|
||||||
|
camera1:
|
||||||
|
- rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0
|
||||||
|
- ffmpeg:camera1#video=mjpeg
|
||||||
|
```
|
||||||
|
|
||||||
|
## API examples
|
||||||
|
|
||||||
|
**MJPEG stream**
|
||||||
|
|
||||||
|
```
|
||||||
|
http://192.168.1.123:1984/api/stream.mjpeg?src=camera1`
|
||||||
|
```
|
||||||
|
|
||||||
|
**JPEG snapshots**
|
||||||
|
|
||||||
|
```
|
||||||
|
http://192.168.1.123:1984/api/frame.jpeg?src=camera1
|
||||||
|
```
|
||||||
|
|
||||||
|
- You can use `width`/`w` and/or `height`/`h` params.
|
||||||
|
- You can use `rotate` param with `90`, `180`, `270` or `-90` values.
|
||||||
|
- You can use `hardware`/`hw` param [read more](https://github.com/AlexxIT/go2rtc/wiki/Hardware-acceleration).
|
||||||
|
- You can use `cache` param (`1m`, `10s`, etc.) to get a cached snapshot.
|
||||||
|
- The snapshot is cached only when requested with the `cache` parameter.
|
||||||
|
- A cached snapshot will be used if its time is not older than the time specified in the `cache` parameter.
|
||||||
|
- The `cache` parameter does not check the image sizes from the cache and those specified in the query.
|
||||||
|
|
||||||
## Stream as ASCII to Terminal
|
## Stream as ASCII to Terminal
|
||||||
|
|
||||||
[](https://www.youtube.com/watch?v=sHj_3h_sX7M)
|
[](https://www.youtube.com/watch?v=sHj_3h_sX7M)
|
||||||
|
|||||||
+46
-3
@@ -6,6 +6,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AlexxIT/go2rtc/internal/api"
|
"github.com/AlexxIT/go2rtc/internal/api"
|
||||||
@@ -36,12 +37,41 @@ func Init() {
|
|||||||
var log zerolog.Logger
|
var log zerolog.Logger
|
||||||
|
|
||||||
func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
|
func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
|
||||||
stream, _ := streams.GetOrPatch(r.URL.Query())
|
query := r.URL.Query()
|
||||||
|
stream, _ := streams.GetOrPatch(query)
|
||||||
if stream == nil {
|
if stream == nil {
|
||||||
http.Error(w, api.StreamNotFound, http.StatusNotFound)
|
http.Error(w, api.StreamNotFound, http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var b []byte
|
||||||
|
|
||||||
|
if s := query.Get("cache"); s != "" {
|
||||||
|
if timeout, err := time.ParseDuration(s); err == nil {
|
||||||
|
src := query.Get("src")
|
||||||
|
|
||||||
|
cacheMu.Lock()
|
||||||
|
entry, found := cache[src]
|
||||||
|
cacheMu.Unlock()
|
||||||
|
|
||||||
|
if found && time.Since(entry.timestamp) < timeout {
|
||||||
|
writeJPEGResponse(w, entry.payload)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
entry = cacheEntry{payload: b, timestamp: time.Now()}
|
||||||
|
cacheMu.Lock()
|
||||||
|
if cache == nil {
|
||||||
|
cache = map[string]cacheEntry{src: entry}
|
||||||
|
} else {
|
||||||
|
cache[src] = entry
|
||||||
|
}
|
||||||
|
cacheMu.Unlock()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
cons := magic.NewKeyframe()
|
cons := magic.NewKeyframe()
|
||||||
cons.WithRequest(r)
|
cons.WithRequest(r)
|
||||||
|
|
||||||
@@ -52,7 +82,7 @@ func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
once := &core.OnceBuffer{} // init and first frame
|
once := &core.OnceBuffer{} // init and first frame
|
||||||
_, _ = cons.WriteTo(once)
|
_, _ = cons.WriteTo(once)
|
||||||
b := once.Buffer()
|
b = once.Buffer()
|
||||||
|
|
||||||
stream.RemoveConsumer(cons)
|
stream.RemoveConsumer(cons)
|
||||||
|
|
||||||
@@ -60,7 +90,7 @@ func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
|
|||||||
case core.CodecH264, core.CodecH265:
|
case core.CodecH264, core.CodecH265:
|
||||||
ts := time.Now()
|
ts := time.Now()
|
||||||
var err error
|
var err error
|
||||||
if b, err = ffmpeg.JPEGWithQuery(b, r.URL.Query()); err != nil {
|
if b, err = ffmpeg.JPEGWithQuery(b, query); err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -69,6 +99,19 @@ func handlerKeyframe(w http.ResponseWriter, r *http.Request) {
|
|||||||
b = mjpeg.FixJPEG(b)
|
b = mjpeg.FixJPEG(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
writeJPEGResponse(w, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
var cache map[string]cacheEntry
|
||||||
|
var cacheMu sync.Mutex
|
||||||
|
|
||||||
|
// cacheEntry represents a cached keyframe with its timestamp
|
||||||
|
type cacheEntry struct {
|
||||||
|
payload []byte
|
||||||
|
timestamp time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeJPEGResponse(w http.ResponseWriter, b []byte) {
|
||||||
h := w.Header()
|
h := w.Header()
|
||||||
h.Set("Content-Type", "image/jpeg")
|
h.Set("Content-Type", "image/jpeg")
|
||||||
h.Set("Content-Length", strconv.Itoa(len(b)))
|
h.Set("Content-Length", strconv.Itoa(len(b)))
|
||||||
|
|||||||
Reference in New Issue
Block a user