Rtsp over websocket

I can’t provide a demo since the camera is in LAN which can’t be access from the world. but camera IPC-HDW3233C (from dahua) web page is using rtspoverwebsocket, it’s quite smooth, much faster than the one from home assistant. maybe home assistant will consider using this feature in frontend

https://www.dahuatech.com/product/info/4273.html

var WebsocketServer = function(a, b) {
    function c() {}
    function d(a, b, c, d) {
        var e = "";
        switch (a) {
        case "OPTIONS":
        case "TEARDOWN":
        case "GET_PARAMETER":
        case "SET_PARAMETERS":
            e = a + " " + M + " RTSP/1.0\r\nCSeq: " + B + "\r\n" + z + "\r\n";
            break;
        case "DESCRIBE":
            e = a + " " + M + " RTSP/1.0\r\nCSeq: " + B + "\r\n" + z + "\r\n";
            break;
        case "SETUP":
            debug.log("trackID: " + b),
            e = a + " " + M + "/trackID=" + b + " RTSP/1.0\r\nCSeq: " + B + "\r\n" + z + "Transport: DH/AVP/TCP;unicast;interleaved=" + 2 * b + "-" + (2 * b + 1) + "\r\n",
            e += 0 != G ? "Session: " + G + "\r\n\r\n" : "\r\n";
            break;
        case "PLAY":
            e = a + " " + M + " RTSP/1.0\r\nCSeq: " + B + "\r\nSession: " + G + "\r\n",
            void 0 != d && 0 != d ? (e += "Range: npt=" + d + "-\r\n",
            e += z + "\r\n") : e += z + "\r\n";
            break;
        case "PAUSE":
            e = a + " " + M + " RTSP/1.0\r\nCSeq: " + B + "\r\nSession: " + G + "\r\n\r\n";
            break;
        case "SCALE":
            e = "PLAY " + M + " RTSP/1.0\r\nCSeq: " + B + "\r\nSession: " + G + "\r\n",
            e += "Scale: " + d + "\r\n",
            e += z + "\r\n"
        }
        return debug.log(e),
        e
    }
    function e(a) {
        debug.log(a);
        var b = {}
          , e = a.search("CSeq: ") + 5;
        if (B = parseInt(a.slice(e, e + 10)) + 1,
        b = m(a),
        b.ResponseCode === x.UNAUTHORIZED && "" === z)
            f(b);
        else if (b.ResponseCode === x.OK) {
            if ("Options" === E)
                return E = "Describe",
                d("DESCRIBE", null, null);
            if ("Describe" === E) {
                I = !1,
                D = n(a),
                "undefined" != typeof b.ContentBase && (D.ContentBase = b.ContentBase);
                var g = 0;
                for (g = 0; g < D.Sessions.length; g += 1) {
                    var i = {};
                    "JPEG" === D.Sessions[g].CodecMime || "H264" === D.Sessions[g].CodecMime || "H265" === D.Sessions[g].CodecMime || "H264-SVC" == D.Sessions[g].CodecMime ? (i.codecName = D.Sessions[g].CodecMime,
                    "H264-SVC" == D.Sessions[g].CodecMime && (i.codecName = "H264"),
                    "H265" == D.Sessions[g].CodecMime && c.prototype.setLiveMode("canvas"),
                    i.trackID = D.Sessions[g].ControlURL,
                    i.ClockFreq = D.Sessions[g].ClockFreq,
                    i.Port = parseInt(D.Sessions[g].Port),
                    "undefined" != typeof D.Sessions[g].Framerate && (i.Framerate = parseInt(D.Sessions[g].Framerate),
                    w.setFPS(i.Framerate),
                    N(i.Framerate)),
                    A.push(i)) : "PCMU" === D.Sessions[g].CodecMime || -1 !== D.Sessions[g].CodecMime.search("G726-16") || -1 !== D.Sessions[g].CodecMime.search("G726-24") || -1 !== D.Sessions[g].CodecMime.search("G726-32") || -1 !== D.Sessions[g].CodecMime.search("G726-40") || "PCMA" === D.Sessions[g].CodecMime ? ("PCMU" === D.Sessions[g].CodecMime ? i.codecName = "G.711Mu" : "G726-16" === D.Sessions[g].CodecMime ? i.codecName = "G.726-16" : "G726-24" === D.Sessions[g].CodecMime ? i.codecName = "G.726-24" : "G726-32" === D.Sessions[g].CodecMime ? i.codecName = "G.726-32" : "G726-40" === D.Sessions[g].CodecMime ? i.codecName = "G.726-40" : "PCMA" === D.Sessions[g].CodecMime && (i.codecName = "G.711A"),
                    i.trackID = D.Sessions[g].ControlURL,
                    i.ClockFreq = D.Sessions[g].ClockFreq,
                    i.Port = parseInt(D.Sessions[g].Port),
                    i.Bitrate = parseInt(D.Sessions[g].Bitrate),
                    A.push(i)) : "mpeg4-generic" === D.Sessions[g].CodecMime || "MPEG4-GENERIC" === D.Sessions[g].CodecMime ? (i.codecName = "mpeg4-generic",
                    i.trackID = D.Sessions[g].ControlURL,
                    i.ClockFreq = D.Sessions[g].ClockFreq,
                    i.Port = parseInt(D.Sessions[g].Port),
                    i.Bitrate = parseInt(D.Sessions[g].Bitrate),
                    A.push(i)) : "vnd.onvif.metadata" === D.Sessions[g].CodecMime ? (i.codecName = "MetaData",
                    i.trackID = D.Sessions[g].ControlURL,
                    i.ClockFreq = D.Sessions[g].ClockFreq,
                    i.Port = parseInt(D.Sessions[g].Port),
                    A.push(i)) : "stream-assist-frame" === D.Sessions[g].CodecMime ? (i.codecName = "stream-assist-frame",
                    i.trackID = D.Sessions[g].ControlURL,
                    i.ClockFreq = D.Sessions[g].ClockFreq,
                    i.Port = parseInt(D.Sessions[g].Port),
                    A.push(i)) : debug.log("Unknown codec type:", D.Sessions[g].CodecMime, D.Sessions[g].ControlURL)
                }
                return F = 0,
                E = "Setup",
                debug.log(A),
                d("SETUP", F)
            }
            if ("Setup" === E) {
                if (G = b.SessionID,
                F < A.length)
                    return A[F].RtpInterlevedID = b.RtpInterlevedID,
                    A[F].RtcpInterlevedID = b.RtcpInterlevedID,
                    F += 1,
                    F !== A.length ? d("SETUP", A[F].trackID.split("=")[1] - 0) : (w.sendSdpInfo(A, L, I),
                    E = "Play",
                    d("PLAY", null));
                debug.log("Unknown setup SDP index")
            } else if ("Play" === E) {
                G = b.SessionID,
                clearInterval(J),
                J = setInterval(function() {
                    return h(d("GET_PARAMETER", null, null))
                }, y);
                E = "Playing"
            } else
                "Playing" === E || debug.log("unknown rtsp state:" + E)
        } else if (b.ResponseCode === x.NOTSERVICE) {
            if ("Setup" === E && -1 !== A[F].trackID.search("trackID=t"))
                return A[F].RtpInterlevedID = -1,
                A[F].RtcpInterlevedID = -1,
                F += 1,
                I = !1,
                C({
                    errorCode: "504",
                    description: "Talk Service Unavilable",
                    place: "RtspClient.js"
                }),
                F < A.length ? d("SETUP", A[F].trackID) : (E = "Play",
                d("PLAY", null));
            C({
                errorCode: "503",
                description: "Service Unavilable"
            })
        } else if (b.ResponseCode === x.NOTFOUND) {
            if ("Describe" === E || "Options" === E)
                return void C({
                    errorCode: 404,
                    description: "rtsp not found"
                })
        } else if (b.ResponseCode === x.INVALID_RANGE)
            return ("backup" === H || "playback" === H) && C({
                errorCode: "457",
                description: "Invalid range"
            }),
            void debug.log("RTP disconnection detect!!!")
    }
    function f(a) {
        var b = O.username
          , c = O.passWord
          , e = {
            Method: null,
            Realm: null,
            Nonce: null,
            Uri: null
        }
          , f = null;
        e = {
            Method: E.toUpperCase(),
            Realm: a.Realm,
            Nonce: a.Nonce,
            Uri: M
        },
        f = g(b, c, e.Uri, e.Realm, e.Nonce, e.Method),
        z = 'Authorization: Digest username="' + b + '", realm="' + e.Realm + '",',
        z += ' nonce="' + e.Nonce + '", uri="' + e.Uri + '", response="' + f + '"',
        z += "\r\n",
        h(d("OPTIONS", null, null))
    }
    function g(a, b, c, d, e, f) {
        var g = null
          , h = null
          , i = null;
        return g = hex_md5(a + ":" + d + ":" + b).toLowerCase(),
        h = hex_md5(f + ":" + c).toLowerCase(),
        i = hex_md5(g + ":" + e + ":" + h).toLowerCase()
    }
    function h(a) {
        if (void 0 != a && null != a && "" != a)
            if (null !== o && o.readyState === WebSocket.OPEN) {
                if (v === !1) {
                    var b = a.search("DESCRIBE");
                    -1 !== b && (u = !0,
                    v = !0)
                }
                void 0 != a && o.send(i(a))
            } else
                debug.log("ws未连接")
    }
    function i(a) {
        for (var b = a.length, c = new Uint8Array(new ArrayBuffer(b)), d = 0; b > d; d++)
            c[d] = a.charCodeAt(d);
        return c
    }
    function j(a) {
        var b = new Uint8Array
          , c = new Uint8Array(a.data);
        for (b = new Uint8Array(c.length),
        b.set(c, 0),
        s = b.length; s > 0; )
            if (36 !== b[0]) {
                var d = String.fromCharCode.apply(null, b)
                  , f = null;
                -1 !== d.indexOf("OffLine:File Over"),
                u === !0 ? (f = d.lastIndexOf("\r\n"),
                u = !1) : f = d.search("\r\n\r\n");
                var g = d.search("RTSP");
                if (-1 === g)
                    return void (b = new Uint8Array);
                if (-1 === f)
                    return void (s = b.length);
                q = b.subarray(g, f + p),
                b = b.subarray(f + p);
                var i = String.fromCharCode.apply(null, q);
                h(e(i)),
                s = b.length
            } else {
                if (r = b.subarray(0, p),
                t = r[2] << 24 | r[3] << 16 | r[4] << 8 | r[5],
                !(t + p <= b.length))
                    return void (s = b.length);
                var j = b.subarray(p, t + p);
                l(r, j),
                b = b.subarray(t + p),
                s = b.length
            }
    }
    function k(a) {
        K = a
    }
    function l(a, b) {
        w.parseRTPData(a, b),
        k(!0)
    }
    function m(a) {
        var b = {}
          , c = 0
          , d = 0
          , e = null
          , f = null
          , g = null;
        if (-1 !== a.search("Content-Type: application/sdp")) {
            var h = a.split("\r\n\r\n");
            g = h[0]
        } else
            g = a;
        var i = g.split("\r\n")
          , j = i[0].split(" ");
        if (j.length > 2 && (b.ResponseCode = parseInt(j[1]),
        b.ResponseMessage = j[2]),
        b.ResponseCode === x.OK) {
            for (c = 1; c < i.length; c++)
                if (f = i[c].split(":"),
                "Public" === f[0])
                    b.MethodsSupported = f[1].split(",");
                else if ("CSeq" === f[0])
                    b.CSeq = parseInt(f[1]);
                else if ("Content-Type" === f[0])
                    b.ContentType = f[1],
                    -1 !== b.ContentType.search("application/sdp") && (b.SDPData = n(a));
                else if ("Content-Length" === f[0])
                    b.ContentLength = parseInt(f[1]);
                else if ("Content-Base" === f[0]) {
                    var k = i[c].search("Content-Base:");
                    -1 !== k && (b.ContentBase = i[c].substr(k + 13))
                } else if ("Session" === f[0]) {
                    var l = f[1].split(";");
                    b.SessionID = parseInt(l[0])
                } else if ("Transport" === f[0]) {
                    var m = f[1].split(";");
                    for (d = 0; d < m.length; d++) {
                        var o = m[d].search("interleaved=");
                        if (-1 !== o) {
                            var p = m[d].substr(o + 12)
                              , q = p.split("-");
                            q.length > 1 && (b.RtpInterlevedID = parseInt(q[0]),
                            b.RtcpInterlevedID = parseInt(q[1]))
                        }
                    }
                } else if ("RTP-Info" === f[0]) {
                    f[1] = i[c].substr(9);
                    var r = f[1].split(",");
                    for (b.RTPInfoList = [],
                    d = 0; d < r.length; d++) {
                        var s = r[d].split(";")
                          , t = {}
                          , u = 0;
                        for (u = 0; u < s.length; u++) {
                            var v = s[u].search("url=");
                            -1 !== v && (t.URL = s[u].substr(v + 4)),
                            v = s[u].search("seq="),
                            -1 !== v && (t.Seq = parseInt(s[u].substr(v + 4)))
                        }
                        b.RTPInfoList.push(t)
                    }
                }
        } else if (b.ResponseCode === x.UNAUTHORIZED)
            for (c = 1; c < i.length; c++)
                if (f = i[c].split(":"),
                "CSeq" === f[0])
                    b.CSeq = parseInt(f[1]);
                else if ("WWW-Authenticate" === f[0]) {
                    var w = f[1].split(",");
                    for (d = 0; d < w.length; d++) {
                        var y = w[d].search("Digest realm=");
                        if (-1 !== y) {
                            e = w[d].substr(y + 13);
                            var z = e.split('"');
                            b.Realm = z[1]
                        }
                        if (y = w[d].search("nonce="),
                        -1 !== y) {
                            e = w[d].substr(y + 6);
                            var A = e.split('"');
                            b.Nonce = A[1]
                        }
                    }
                }
        return b
    }
    function n(a) {
        var b = {}
          , c = [];
        b.Sessions = c;
        var d = null;
        if (-1 !== a.search("Content-Type: application/sdp")) {
            var e = a.split("\r\n\r\n");
            d = e[1]
        } else
            d = a;
        var f = d.split("\r\n")
          , g = 0
          , h = !1;
        for (g = 0; g < f.length; g++) {
            var i = f[g].split("=");
            if (i.length > 0)
                switch (i[0]) {
                case "a":
                    var j = i[1].split(":");
                    if (j.length > 1)
                        if ("control" === j[0]) {
                            var k = f[g].search("control:");
                            h === !0 ? -1 !== k && (b.Sessions[b.Sessions.length - 1].ControlURL = f[g].substr(k + 8)) : -1 !== k && (b.BaseURL = f[g].substr(k + 8))
                        } else if ("rtpmap" === j[0]) {
                            var l = j[1].split(" ");
                            b.Sessions[b.Sessions.length - 1].PayloadType = l[0];
                            var m = l[1].split("/");
                            b.Sessions[b.Sessions.length - 1].CodecMime = m[0],
                            m.length > 1 && (b.Sessions[b.Sessions.length - 1].ClockFreq = m[1])
                        } else if ("framesize" === j[0]) {
                            var n = j[1].split(" ");
                            if (n.length > 1) {
                                var o = n[1].split("-");
                                b.Sessions[b.Sessions.length - 1].Width = o[0],
                                b.Sessions[b.Sessions.length - 1].Height = o[1]
                            }
                        } else if ("framerate" === j[0])
                            b.Sessions[b.Sessions.length - 1].Framerate = j[1];
                        else if ("fmtp" === j[0]) {
                            var p = f[g].split(" ");
                            if (p.length < 2)
                                continue;
                            for (var q = 1; q < p.length; q++) {
                                var r = p[q].split(";")
                                  , s = 0;
                                for (s = 0; s < r.length; s++) {
                                    var t = r[s].search("mode=");
                                    if (-1 !== t && (b.Sessions[b.Sessions.length - 1].mode = r[s].substr(t + 5)),
                                    t = r[s].search("config="),
                                    -1 !== t && (b.Sessions[b.Sessions.length - 1].config = r[s].substr(t + 7),
                                    L.config = b.Sessions[b.Sessions.length - 1].config,
                                    L.clockFreq = b.Sessions[b.Sessions.length - 1].ClockFreq,
                                    L.bitrate = b.Sessions[b.Sessions.length - 1].Bitrate),
                                    t = r[s].search("sprop-vps="),
                                    -1 !== t && (b.Sessions[b.Sessions.length - 1].VPS = r[s].substr(t + 10)),
                                    t = r[s].search("sprop-sps="),
                                    -1 !== t && (b.Sessions[b.Sessions.length - 1].SPS = r[s].substr(t + 10)),
                                    t = r[s].search("sprop-pps="),
                                    -1 !== t && (b.Sessions[b.Sessions.length - 1].PPS = r[s].substr(t + 10)),
                                    t = r[s].search("sprop-parameter-sets="),
                                    -1 !== t) {
                                        var u = r[s].substr(t + 21)
                                          , v = u.split(",");
                                        v.length > 1 && (b.Sessions[b.Sessions.length - 1].SPS = v[0],
                                        b.Sessions[b.Sessions.length - 1].PPS = v[1])
                                    }
                                }
                            }
                        }
                    break;
                case "m":
                    var w = i[1].split(" ")
                      , x = {};
                    x.Type = w[0],
                    x.Port = w[1],
                    x.Payload = w[3],
                    b.Sessions.push(x),
                    h = !0;
                    break;
                case "b":
                    if (h === !0) {
                        var y = i[1].split(":");
                        b.Sessions[b.Sessions.length - 1].Bitrate = y[1]
                    }
                }
        }
        return b
    }
    var a = a
      , o = null
      , p = 6
      , q = null
      , r = null
      , s = 0
      , t = 0
      , u = !1
      , v = !1
      , w = new WorkerManager
      , x = {
        OK: 200,
        UNAUTHORIZED: 401,
        NOTFOUND: 404,
        INVALID_RANGE: 457,
        NOTSERVICE: 503,
        DISCONNECT: 999
    }
      , y = 4e4
      , z = ""
      , A = []
      , B = 1
      , C = null
      , D = {}
      , E = "Options"
      , F = null
      , G = null
      , H = ""
      , I = !1
      , J = null
      , K = !1
      , L = {}
      , M = b
      , N = null
      , O = {}
      , P = "";
    return c.prototype = {
        init: function(a, b) {
            w.init(a, b)
        },
        connect: function() {
            o || (o = new WebSocket(a),
            o.binaryType = "arraybuffer",
            o.addEventListener("message", j, !1),
            o.onopen = function() {
                var a = i("OPTIONS " + M + " RTSP/1.0\r\nCSeq: " + B + "\r\n\r\n");
                o.send(a)
            }
            ,
            o.onerror = function() {
                w.openWebSocketError()
            }
            )
        },
        disconnect: function() {
            h(d("TEARDOWN", null, null)),
            clearInterval(J),
            J = null,
            null !== o && o.readyState === WebSocket.OPEN && (o.close(),
            o = null,
            G = null),
            w.terminate()
        },
        controlPlayer: function(a) {
            var b = "";
            switch (P = a.command,
            a.command) {
            case "PLAY":
                if (E = "Play",
                null != a.range) {
                    b = d("PLAY", null, null, a.range);
                    break
                }
                b = d("PLAY", null, null),
                P && w.initStartTime();
                break;
            case "PAUSE":
                if ("PAUSE" === E)
                    break;
                E = "PAUSE",
                b = d("PAUSE", null, null);
                break;
            case "SCALE":
                b = d("SCALE", null, null, a.data),
                w.playbackSpeed(a.data);
                break;
            case "TEARDOWN":
                b = d("TEARDOWN", null, null);
                break;
            case "audioPlay":
            case "volumn":
            case "audioSamplingRate":
                w.controlAudio(a.command, a.data);
                break;
            default:
                debug.log("未知指令: " + a.command)
            }
            "" != b && h(b)
        },
        setLiveMode: function(a) {
            w.setLiveMode(a)
        },
        setRTSPURL: function(a) {
            M = a
        },
        setCallback: function(a, b) {
            "GetFrameRate" === a ? N = b : w.setCallback(a, b),
            "Error" == a && (C = b)
        },
        setUserInfo: function(a, b) {
            O.username = a,
            O.passWord = b
        },
        capture: function(a) {
            w.capture(a)
        }
    },
    new c
};

hello, there is github repo where all related code is already extracted: GitHub - TeamDotworld/dahua-rtsp-web: Stream video in web browser

I recently build an Angular wrapped around it and it works without issues.
HomeAssistant frontend is a bit different but I thinks it’s possible to build such functionality using custom cards.