This post documents the troubleshooting process for an H.265 video stream integration issue in a browser on an IIoT platform. The constraint was that the frontend must not expose high-privilege access credentials. The observed behavior was: the upstream could return a playback URL, desktop players could play it, a local .flv file could play, but browser-based live streaming still failed.

Note: URLs, paths, file names, parameters, headers, authentication data, log content, and timestamps in this post have been sanitized, abstracted, or rewritten. Only the technical details required to explain the troubleshooting path are retained.

Background And Constraints

The integration design was constrained by the following requirements:

  1. The page had to play H.265 video streams
  2. The frontend could not hold a high-privilege Ezviz AT

For that reason, the private protocol approach that requires the frontend to hold privileged credentials was not used. The final path was HTTP-FLV + Jessibuca:

  • The backend requests the playback URL from Ezviz Cloud
  • The backend controls the time-bounded playback URL and parameters
  • The frontend only consumes a controlled playback URL or proxied local URL
  • The browser completes WASM decoding through Jessibuca in the page

The simplified chain is:

[Ezviz Cloud] ---> [Backend gets playback URL] ---> [Local Proxy / Controlled URL]
                                                     |
                                                     v
                                           [Browser + Jessibuca + WASM]

Conclusions

  • A playback URL returned by Ezviz Cloud is not automatically the final URL a browser can consume directly; the desktop player validation step still required the &supportH265=1 parameter
  • The browser could play a same-origin local .flv file, which showed that the Chrome + Jessibuca + WASM H.265 decode path was functional in the current environment
  • When browser live streaming failed, fetchError, proxy-side WinError 10053, and page-side TypeError were part of the same failure chain, not three separate problems
  • The root cause was that the player emitted the kBps event as a string, while the page listener treated it as a number and called .toFixed(), which threw and then propagated into the fetch read path and triggered AbortController
  • The fix included backend-controlled playback URLs, plus frontend type conversion, boundary checks, and exception isolation for third-party event payloads

Troubleshooting Path

The issue was narrowed down in the following order:

  1. Obtain the playback URL from Ezviz Cloud and confirm the upstream can return a live stream URL
  2. Use PotPlayer / VLC Media Player to verify that the live stream is consumable and add required parameters
  3. Play a local .flv file in the browser to establish the H.265 browser baseline
  4. Minimize proxy passthrough logic and remove proxy formatting as a confounding factor
  5. Use browser Console, Network, and player logs together to locate the frontend event-chain failure

This order is meant for layered validation so protocol, proxy, browser, and player implementation issues do not get mixed together too early.

Detailed Process

T0: Define The Solution Boundary

Goal: define the browser-side access boundary.

Constraints:

  • The backend requests the playback URL from Ezviz Cloud
  • The frontend does not request playback URLs from Ezviz Cloud directly
  • The page only consumes controlled live stream URLs
  • The browser uses Jessibuca to play HTTP-FLV

Conclusion: this stage only defined the access boundary.

T1: Verify That The Live Stream Itself Is Consumable

Goal: confirm that the playback URL returned by the upstream can be consumed by a client.

Method: use PotPlayer to validate the live stream.

The initial validation failed. After adding the following parameters, PotPlayer could play the live stream:

  • protocol=4 // Request HTTP-FLV explicitly; the default is the Ezviz private video protocol
  • &supportH265=1 related passthrough parameter

Observations:

  1. The URL returned by Ezviz Cloud was not invalid
  2. The live stream itself was consumable on the desktop-player side

Conclusion: the problem scope was reduced to the browser chain.

T2: Browser Live Stream Failed, But Browser Decode Was Not Assumed As The Cause

Goal: determine which layer the browser live-stream failure belonged to.

Observations:

  • Page initialization was normal
  • The playback request was issued successfully
  • The live stream failed after a short period
  • The page reported fetchError
  • The proxy reported WinError 10053

Possible causes:

  • Browser decode capability
  • Proxy passthrough format
  • Active request cancellation
  • Internal player event handling

Conclusion: fetchError alone was not enough to distinguish protocol, network, or frontend logic issues.

T3: Establish A Browser Playback Baseline With A Local FLV File

Goal: rule out a hard H.265 decode block in the browser.

Method: play a locally downloaded .flv file from the monitoring platform directly in the page.

Purpose:

  • Keep media content consistent with the real chain
  • Validate the page decode path of Jessibuca + WASM + Chrome

Observation: the local .flv file played normally.

Conclusions:

  • The current browser environment could handle this kind of H.265 content
  • Jessibuca base loading, WASM initialization, and rendering all worked
  • The problem was concentrated in the processing path after the live stream entered the player

T4: Reduce The Proxy To Minimal Passthrough

Goal: verify whether the proxy layer introduced extra issues.

Method: reduce the proxy implementation to the minimum, keeping only response header setup and raw stream writing:

self.send_response(200)
self.send_header("Content-Type", "video/x-flv")
self.end_headers()
self.wfile.write(chunk)
self.wfile.flush()

At the same time, remove the manually assembled chunked block header logic.

Observation: after this change, browser live streaming still failed.

Conclusion: proxy passthrough format was not the final root cause. Manual chunked block assembly was a concurrent issue. Fixing it removed one protocol-layer source of noise, but it still could not explain why the browser actively canceled the request.

T5: Locate The Trigger In The Browser Event Chain

Goal: locate the direct trigger of the live-stream failure.

The key evidence to inspect was:

  • Whether Console showed uncaught exceptions
  • Whether the stream request in Network was marked as cancelled
  • Which event the player log showed immediately before failure

Method: enable player debug output and inspect the event sequence before failure.

Observation: once the throughput-statistics event fired for the first time, the page consistently threw a TypeError.

These signals could now be placed on the same timeline:

  • Page layer: fetchError
  • Proxy layer: WinError 10053
  • Browser layer: TypeError

Conclusion: the trigger point was in the frontend event-handling chain, not the proxy passthrough chain.

Root Cause Analysis

1. The kBps Event Payload Was A String

The player periodically calculates throughput and emits the kBps event. Internally, the value had already been formatted into a string:

player.emit('kBps', (rate / 1000).toFixed(2));

toFixed() returns a string, so the payload shape was "867.84" rather than 867.84.

2. The Page Listener Assumed The Payload Was Numeric

The page handled the value as if it were a number:

player.on('kBps', function(kbps) {
    statBitrate.innerText = kbps.toFixed(1);
});

When kbps was actually a string, the browser threw:

TypeError: kbps.toFixed is not a function

3. The Page Exception Entered The Player Read-Error Path

The exception then propagated along the player’s stream-read chain and entered the unified exception path:

reader.read().then(({ done, value }) => {
    // process chunk
}).catch((e) => {
    this.abort();
    this.emit('fetchError', e);
});

This logic did not distinguish between two different exception sources:

  • Low-level stream read failures
  • Exceptions thrown by external event callbacks

Result: the TypeError thrown by the page listener was handled as a stream read failure.

4. AbortController Terminated The Downstream Connection

Once this.abort() executed, the following chain occurred:

  • AbortController emitted a cancel signal
  • The browser aborted the current fetch
  • The downstream TCP connection was actively closed by the browser
  • The proxy kept writing the stream and therefore logged WinError 10053

The relationship was:

[Proxy writes stream]
       |
       v
[Fetch reader]
       |
       v
[Player emits kBps]
       |
       v
[Page listener throws TypeError]
       |
       v
[reader.read().catch(...)]
       |
       v
[AbortController abort()]
       |
       v
[Browser closes downstream TCP]

Conclusion: this was not “proxy failure caused stream interruption”. It was “page-side event handling failure caused the browser to actively cancel the request”.

Stable Reproduction Point

Observation: the failure usually reproduced after the first few chunks, and the reproduction point was stable.

Reason:

  • The first few chunks were used to establish playback and statistics context
  • The kBps event fired for the first time when accumulated data reached roughly 32~40KB
  • The page listener entered the faulty path at that moment

Conclusion: the trigger was not a specific chunk, but the moment when the first throughput callback fired. In this chain, that moment roughly matched the first statistics window after the first 5 chunks.

Fix

The fix covered both chain boundaries and event-handling behavior:

  1. The backend requests the playback URL from Ezviz Cloud
  2. The backend controls playback URL lifetime and parameters instead of exposing a privileged AT to the frontend
  3. The frontend only consumes controlled live stream URLs
  4. The browser continues to use Jessibuca for HTTP-FLV
  5. The page applies type conversion and exception isolation to third-party player callbacks

The direct frontend fix for the kBps event was:

player.on('kBps', function(kbps) {
    const value = parseFloat(kbps);
    statBitrate.innerText = isNaN(value) ? '0.0' : value.toFixed(1);
});

A further defensive measure is to wrap external callbacks in exception isolation:

player.on('eventName', function(payload) {
    try {
        // business logic
    } catch (e) {
        console.error('[player-event-error]', e);
    }
});

This prevents page-layer exceptions from flowing back into the player’s core read path.

Troubleshooting Flow

If a monitoring cloud platform can return a live stream URL but browser playback still fails, use the following troubleshooting flow:

Upstream playback URL -> Desktop player validation -> Local FLV baseline -> Minimal proxy passthrough -> Console / Network / player log correlation -> Event payload type and exception isolation checks

Key checks:

  • If the desktop player cannot play the stream, go back to playback URL parameters and protocol compatibility before moving into the browser layer
  • If a local .flv file plays but the live stream does not, prioritize the live stream read path, request cancellation path, and player event chain
  • Keep the proxy layer minimal during isolation so chunked wrapping, statistics logic, or other intermediate processing do not distort the conclusion
  • If Console shows a runtime exception and Network shows the corresponding request as cancelled, prioritize page logic and player callback inspection
  • If fetchError, WinError 10053, and a frontend exception appear in close sequence, analyze them on the same timeline instead of treating them as separate problems
  • Apply type conversion, boundary checks, and exception isolation to third-party callbacks by default instead of assuming the payload type is stable

Validation Results And Boundaries

Validation results in the current environment were:

  • The controlled playback URL could be integrated into the IIoT platform page
  • PotPlayer could validate that the live stream met consumption requirements
  • The browser could play H.265 HTTP-FLV live streams
  • Representative observations were about 2K and 29 FPS
  • End-to-end latency observations were on the order of 200ms
  • The same WinError 10053 did not reappear during this validation

These results only apply to the current environment and should not be generalized to other network conditions, browser versions, or upstream parameter combinations.

Retained Constraints

  • Keep the troubleshooting order as “upstream URL -> desktop player -> local FLV -> live stream event chain”

  • The root cause was an event callback without type defense that brought a page-layer exception into the stream-read chain and was then handled by the player as a network-error path

  • Keep backend control over playback URL generation, parameters, and expiration policy

  • Keep both desktop-player and browser paths for live-stream validation

  • Apply exception isolation uniformly to external player callbacks

  • Log payload type and raw value when key events fire for the first time

  • Keep the proxy layer minimal in both passthrough behavior and state