एक प्रोग्राम बनाने के बाद जो multipart/x-mixed-replace Content-Type हेडर के माध्यम से पीएनजी छवियों को ब्राउज़र में स्ट्रीम करता है, मैंने देखा कि <img> टैग में केवल फ्रेम पहले-आखिरी प्रदर्शित होता है, इसके विपरीत सबसे हाल ही में भेजा गया।

यह व्यवहार बहुत कष्टप्रद है, क्योंकि मैं केवल अपडेट भेज रहा हूं जब छवि बैंडविड्थ पर सहेजने के लिए बदलती है, जिसका अर्थ है कि गलत फ्रेम स्क्रीन पर होगा जबकि मैं इसके अपडेट होने की प्रतीक्षा कर रहा हूं।

विशेष रूप से, मैं बहादुर ब्राउज़र (क्रोमियम पर आधारित) का उपयोग कर रहा हूं, लेकिन जैसा कि मैंने ऊपर और नीचे दोनों "ढाल" के साथ प्रयास किया है, मुझे लगता है कि यह समस्या कम से कम अन्य क्रोमियम-आधारित ब्राउज़रों में भी होती है।

समस्या की खोज करने पर केवल एक प्रासंगिक परिणाम मिलता है (और कई गैर-प्रासंगिक) जो यह HowToForge थ्रेड, बिना किसी उत्तर के। इसी तरह, मैंने यह भी सोचा कि मुद्दा बफरिंग के साथ करना है, लेकिन मैंने बफर को बिना किसी लाभ के फ्लश करना सुनिश्चित किया, थ्रेड में उपयोगकर्ता के समान ही। उपयोगकर्ता रिपोर्ट करता है कि यह उनके सर्वरों में से एक पर काम करता है और दूसरे पर नहीं, जो तब मुझे विश्वास दिलाता है कि यह एक विशिष्ट HTTP शीर्षलेख या उन पंक्तियों के साथ कुछ करने के लिए हो सकता है। मेरा पहला अनुमान Content-Length था क्योंकि ब्राउज़र यह बता सकता है कि छवि कब से पूरी हो गई है, लेकिन इसका कोई असर नहीं हुआ।

तो अनिवार्य रूप से, मेरा प्रश्न है: क्या ब्राउज़र को सबसे हाल का multipart/x-mixed-replace दिखाने के लिए कहने का कोई तरीका है, न कि पहले वाला? और, यदि यह मानक व्यवहार नहीं है, तो इसका क्या कारण हो सकता है?

और निश्चित रूप से, यहां प्रासंगिक स्रोत कोड है, हालांकि मुझे लगता है कि यह कोड के साथ एक से अधिक सामान्य HTTP प्रश्न है:

सर्वर

package routes

import (
    "crypto/md5"
    "fmt"
    "image/color"
    "net/http"
    "time"

    brain "path/to/image/generator/module"
)

func init() {
    RouteHandler{
        function: func(w http.ResponseWriter, r *http.Request) {
            w.Header().Set("Content-Type", "multipart/x-mixed-replace; boundary=frame")
            w.Header().Set("Cache-Control", "no-cache") // <- Just in case
            w.WriteHeader(200)

            // If the request contains a token and the token maps to a valid "brain", start consuming frames from
            // the brain and returning them to the client
            params := r.URL.Query()
            if val, ok := params["token"]; ok && len(val) > 0 {
                if b, ok := SharedMemory["brains"].(map[string]*brain.Brain)[val[0]]; ok && !b.CheckHasExit() {
                    // Keep a checksum of the previous frame to avoid sending frames which haven't changed. Frames cannot
                    // be compared directly (at least efficiently) as they are slices not arrays
                    previousFrameChecksum := [16]byte{}

                    for {
                        if !b.CheckHasExit() {
                            frame, err := b.GetNextFrame(SharedMemory["conf"].(map[string]interface{})["DISPLAY_COL"].(color.Color))
                            if err == nil && md5.Sum(frame) != previousFrameChecksum {
                                // Only write the frame if we succesfully read it and it's different to the previous
                                _, err = w.Write([]byte(fmt.Sprintf("--frame\r\nContent-Type: image/png\r\nContent-Size: %d\r\n\r\n%s\r\n", len(frame), frame)))
                                if err != nil {
                                    // The client most likely disconnected, so we should end the stream. As the brain still exists, the
                                    // user can re-connect at any time
                                    return
                                }
                                // Update the checksum to this frame
                                previousFrameChecksum = md5.Sum(frame)
                                // If possible, flush the buffer to make sure the frame is sent ASAP
                                if flusher, ok := w.(http.Flusher); ok {
                                    flusher.Flush()
                                }
                            }
                            // Limit the framerate to reduce CPU usage
                            <-time.After(time.Duration(SharedMemory["conf"].(map[string]interface{})["FPS_LIMITER_INTERVAL"].(int)) * time.Millisecond)
                        } else {
                            // The brain has exit so there is no more we can do - we are braindead :P
                            return
                        }
                    }
                }
            }
        },
    }.Register("/stream", "/stream.png")
}

क्लाइंट (start() शरीर में चलता है onload)

function start() {
    // Fetch the token from local storage. If it's empty, the server will automatically create a new one
    var token = localStorage.getItem("token");
    // Create a session with the server
    http = new XMLHttpRequest();
    http.open("GET", "/startsession?token="+(token)+"&w="+(parent.innerWidth)+"&h="+(parent.innerHeight));
    http.send();
    http.onreadystatechange = (e) => {
        if (http.readyState === 4 && http.status === 200) {
            // Save the returned token
            token = http.responseText;
            localStorage.setItem("token", token);
            // Create screen
            var img = document.createElement("img");
            img.alt = "main display";
            // Hide the loader when it loads
            img.onload = function() {
                var loader = document.getElementById("loader");
                loader.remove();
            }
            // Start loading
            img.src = "/stream.png?token="+token;
            // Start capturing keystrokes
            document.onkeydown = function(e) {
                // Send the keypress to the server as a command (ignore the response)
                cmdsend = new XMLHttpRequest();
                cmdsend.open("POST", "/cmd?token="+(token));
                cmdsend.send("keypress:"+e.code);
                // Catch special cases
                if (e.code === "Escape") {
                    // Clear local storage to remove leftover token
                    localStorage.clear();
                    // Remove keypress handler
                    document.onkeydown = function(e) {}
                    // Notify the user
                    alert("Session ended succesfully and the screen is inactive. You may now close this tab.");
                }
                // Cancel whatever it is the keypress normally does
                return false;
            }
            // Add screen to body
            document.getElementById("body").appendChild(img);
        } else if (http.readyState === 4) {
            alert("Error while starting the session: "+http.responseText);
        }
    }
}
1
user9123 7 जिंदा 2021, 00:56
ऐसा लगता है कि यह एक कैशिंग मुद्दा हो सकता है। get param के रूप में कुछ यादृच्छिक जंक जोड़कर img.src url को अद्वितीय बनाएं।
 – 
John
7 जिंदा 2021, 01:03
इसने मेरे दिमाग को पार कर लिया, लेकिन छवि बदल जाती है, यह सिर्फ एक फ्रेम को बहुत देर से बदलता है, इसलिए यह संभावना नहीं है कि यह आईएमओ को कैशिंग कर रहा है। उसके ऊपर, प्रोग्राम के बीच टोकन परिवर्तन पुनरारंभ होता है (और यह एक GET परम है) ताकि कुछ हद तक कैशिंग बंद हो जाए, और फिर मैंने अच्छे उपाय के लिए Cache-Control: no-cache हेडर भी जोड़ा।
 – 
user9123
7 जिंदा 2021, 01:15

3 जवाब

सबसे बढ़िया उत्तर

मल्टीपार्ट MIME संदेश के अंदर एक भाग MIME हेडर से शुरू होता है और सीमा के साथ समाप्त होता है। पहले वास्तविक भाग से पहले एक ही सीमा होती है। यह प्रारंभिक सीमा MIME प्रस्तावना को बंद कर देती है।

इसके बजाय आपका कोड मानता है कि सीमा के साथ एक हिस्सा शुरू होता है। इस धारणा के आधार पर आप पहले बाउंड्री भेजते हैं, फिर MIME हेडर और फिर MIME बॉडी। फिर आप अगला भाग तैयार होने तक भेजना बंद कर दें। इस वजह से एक भाग के अंत का पता तभी चलेगा जब आप अगला भाग भेजेंगे, तभी से आप पिछले भाग की अंतिम सीमा भेजेंगे।

इसे ठीक करने के लिए आपके कोड को शुरू में MIME प्रस्तावना को समाप्त करने के लिए एक सीमा भेजनी चाहिए। प्रत्येक नए भाग के लिए इसे MIME शीर्षलेख, MIME निकाय और फिर इस भाग को समाप्त करने के लिए सीमा भेजनी चाहिए।

4
Steffen Ullrich 7 जिंदा 2021, 02:04
देरी के लिए खेद है, और उत्तर के लिए धन्यवाद। मुझे लगता है कि मैंने वास्तव में कुछ हद तक कोशिश की थी जो आपने पूछने से पहले वर्णित किया था, लेकिन यह काम नहीं कर रहा था, इसलिए मैंने परीक्षण के अंत में सीमा को रखा। वैसे भी, आपने जो कहा वह सही लगता है, लेकिन इसे बदलने के बाद भी मुझे वही समस्या हो रही है। मैंने कुछ कर्ल डिबगिंग किया है, और पाया कि जब मैं अंत में सीमा शामिल करता हूं, तो यह अगले अनुरोध तक नहीं भेजा जाता है, यहां तक ​​​​कि उसी बाइट्स का हिस्सा भी होता है। कोई विचार ऐसा क्यों हो सकता है?
 – 
user9123
7 जिंदा 2021, 19:30
मुझे एहसास है कि मैंने जो लिखा है उसका पालन करना मुश्किल हो सकता है, इसलिए यह रहा अपडेट कोड, cURL लॉग और प्रोग्राम लॉग जो उम्मीद से स्पष्ट कर सकता है। मुद्दा यह है कि भले ही मैं अब --frame ट्रेलर भेज रहा हूं, लेकिन यह वास्तव में किसी कारण से नहीं भेजा जाता है।
 – 
user9123
7 जिंदा 2021, 19:36
एक और अद्यतन - अंत में दो नई पंक्तियाँ जोड़ना (--frame के बाद) --frame के नहीं भेजे जाने की समस्या को हल करता है, लेकिन अब क्योंकि --frame के बाद 2 नई पंक्तियाँ हैं, ब्राउज़र अब और नहीं कर सकता प्रतिक्रिया पढ़ें।
 – 
user9123
7 जिंदा 2021, 19:47
@ user9123: सीमा को \r\n--frame\r\n के रूप में भेजने की आवश्यकता है, न कि \r\n--frame जैसा कि वर्तमान में किया गया है, अर्थात पंक्ति का अंत बाद सीमा भी महत्वपूर्ण है। निश्चित रूप से सीमा के बाद MIME शीर्षलेख केवल पहली शीर्षलेख पंक्ति से प्रारंभ होना चाहिए न कि किसी अन्य \r\n से।
 – 
Steffen Ullrich
7 जिंदा 2021, 19:53
मैंने अपना कोड एक बार फिर अपडेट किया है, इस बार गो के मानक mime/multipart पैकेज का उपयोग करने के लिए, इसलिए मैंने प्रोटोकॉल के साथ अब कोई समस्या नहीं होनी चाहिए। फिर भी मैं 1 फ्रेम पीछे हूं (छवि तब तक लोड नहीं होती जब तक कि मैं अपडेट ट्रिगर करने के लिए स्पेस दबाता हूं)। मुझे ऐसा लगता है कि इसका संबंध flusher.Flush() से है जो काम नहीं कर रहा है, लेकिन मुझे नहीं पता कि ऐसा क्यों होगा। कोई विचार?
 – 
user9123
7 जिंदा 2021, 21:18

मुझे भी यही समस्या थी: multipart/x-mixed-replace का उपयोग करते समय 1 फ्रेम विलंब होता है

ऐसा लगता है कि यह समस्या Chrome में दिखाई दे रही है और यह Chrome no से संबंधित है लंबे समय तक समर्थन multipart/x-mixed-replace संसाधन। यह समस्या फ़ायरफ़ॉक्स में मौजूद नहीं है।

इसलिए, वीडियो स्ट्रीम प्रदर्शित करने में क्रोम को "चाल" करने का एकमात्र तरीका प्रत्येक छवि को दो बार भेजना या स्वीकार करना है कि 1 फ्रेम देरी होगी। जैसा कि कहा गया है, समस्या फ़ायरफ़ॉक्स में मौजूद नहीं है।

1
David Torres 12 मई 2021, 18:09

यह क्रोम के साथ एक मुद्दा है। फ़ायरफ़ॉक्स में यह अपेक्षा के अनुरूप काम किया।

मैंने इसके आसपास निम्नलिखित तरीके से काम किया C# example

var chromeWorkaround = Encoding.UTF8.GetBytes($"\r\n--{Boundary}\r\n\r\n--{Boundary}\r\n");

इसे अपनी स्ट्रीम में जोड़ें और ऐसा लगता है कि क्रोम को तुरंत प्रस्तुत करने के लिए मजबूर किया गया है।

मैंने इसकी यहां रिपोर्ट की: https://bugs.chromium.org/p /क्रोमियम/मुद्दों/विस्तार?id=1250396

0
Red Riding Hood 16 सितंबर 2021, 20:36