feat(HikVisionWebPlugin): add OnInitedAsync parameter#785
Conversation
Reviewer's GuideRefactors the HikVision Web plugin into a dedicated HikVisionWebPlugin component with richer state management, adds an OnInitedAsync callback and JS-to-.NET init flow, and tightens the JS SDK integration to return explicit success flags and guard operations based on initialization, login, and real-play state. Sequence diagram for HikVisionWebPlugin initialization with OnInitedAsync callbacksequenceDiagram
participant BlazorPage
participant HikVisionWebPlugin
participant JSRuntime
participant HikVisionWebPlugin_js
participant Hikvision_js
participant WebVideoCtrl
BlazorPage->>HikVisionWebPlugin: Render component
HikVisionWebPlugin->>JSRuntime: InvokeAsync init(Id, DotNetObjectRef)
JSRuntime->>HikVisionWebPlugin_js: init(id, invoke)
HikVisionWebPlugin_js->>Hikvision_js: initVision(id)
Hikvision_js->>WebVideoCtrl: I_InjectPlugin(id)
WebVideoCtrl-->>Hikvision_js: Plugin window inited or failed
Hikvision_js-->>HikVisionWebPlugin_js: inited (true or false)
HikVisionWebPlugin_js->>JSRuntime: invoke.invokeMethodAsync TriggerInited(inited)
JSRuntime->>HikVisionWebPlugin: TriggerInited(inited)
HikVisionWebPlugin->>HikVisionWebPlugin: Inited = inited
alt OnInitedAsync assigned
HikVisionWebPlugin->>BlazorPage: OnInitedAsync(inited)
end
Sequence diagram for login and real-play operations with state guardssequenceDiagram
participant BlazorPage
participant HikVisionWebPlugin
participant JSRuntime
participant HikVisionWebPlugin_js
participant Hikvision_js
participant WebVideoCtrl
rect rgb(230,230,255)
BlazorPage->>HikVisionWebPlugin: Login(ip, port, userName, password, loginType)
HikVisionWebPlugin->>HikVisionWebPlugin: ThrowIfNotInited()
HikVisionWebPlugin->>JSRuntime: InvokeAsync<bool?> login(Id, ip, port, userName, password, loginType)
JSRuntime->>HikVisionWebPlugin_js: login(Id, ip, port, userName, password, loginType)
HikVisionWebPlugin_js->>Hikvision_js: login(Id, ip, port, userName, password, loginType)
Hikvision_js->>Hikvision_js: validate inited and parameters
alt not inited or invalid
Hikvision_js-->>HikVisionWebPlugin_js: false
else already logined
Hikvision_js-->>HikVisionWebPlugin_js: true
else perform login
Hikvision_js->>WebVideoCtrl: I_Login(szDeviceIdentify, options)
WebVideoCtrl-->>Hikvision_js: success or error
Hikvision_js-->>HikVisionWebPlugin_js: vision.logined (true or false)
end
HikVisionWebPlugin_js-->>JSRuntime: bool
JSRuntime-->>HikVisionWebPlugin: bool
HikVisionWebPlugin->>HikVisionWebPlugin: IsLogined = result
HikVisionWebPlugin-->>BlazorPage: bool
end
rect rgb(230,255,230)
BlazorPage->>HikVisionWebPlugin: StartRealPlay(streamType, channelId)
HikVisionWebPlugin->>HikVisionWebPlugin: check IsLogined && !IsRealPlaying
alt allowed to start
HikVisionWebPlugin->>JSRuntime: InvokeAsync<bool?> startRealPlay(Id, streamType, channelId)
JSRuntime->>HikVisionWebPlugin_js: startRealPlay(Id, streamType, channelId)
HikVisionWebPlugin_js->>Hikvision_js: startRealPlay(Id, streamType, channelId)
Hikvision_js->>WebVideoCtrl: I_StartRealPlay(szDeviceIdentify, options)
WebVideoCtrl-->>Hikvision_js: success or error
Hikvision_js->>Hikvision_js: set vision.realPlaying and completed
Hikvision_js-->>HikVisionWebPlugin_js: completed (true or false)
HikVisionWebPlugin_js-->>JSRuntime: bool
JSRuntime-->>HikVisionWebPlugin: bool
HikVisionWebPlugin->>HikVisionWebPlugin: IsRealPlaying = result
else guard blocks call
HikVisionWebPlugin-->>BlazorPage: no-op
end
end
rect rgb(255,230,230)
BlazorPage->>HikVisionWebPlugin: StopRealPlay()
HikVisionWebPlugin->>HikVisionWebPlugin: check IsLogined && IsRealPlaying
alt allowed to stop
HikVisionWebPlugin->>JSRuntime: InvokeAsync<bool?> stopRealPlay(Id)
JSRuntime->>HikVisionWebPlugin_js: stopRealPlay(Id)
HikVisionWebPlugin_js->>Hikvision_js: stopRealPlay(Id)
Hikvision_js->>WebVideoCtrl: I_Stop(options)
WebVideoCtrl-->>Hikvision_js: success or error
Hikvision_js-->>HikVisionWebPlugin_js: completed (true or false)
HikVisionWebPlugin_js-->>JSRuntime: bool
JSRuntime-->>HikVisionWebPlugin: bool
HikVisionWebPlugin->>HikVisionWebPlugin: update IsRealPlaying
else guard blocks call
HikVisionWebPlugin-->>BlazorPage: no-op
end
end
Updated class diagram for HikVisionWebPlugin componentclassDiagram
class HikVisionWebPlugin {
+string? IP
+int Port
+string? UserName
+string? Password
+LoginType LoginType
+string? Width
+string? Height
+Func~bool, Task~ OnInitedAsync
+bool Inited
+bool IsLogined
+bool IsRealPlaying
-string? ClassString
-string? StyleString
+Task<bool> Login(string ip, int port, string userName, string password, LoginType loginType)
+Task Logout()
+Task StartRealPlay(int streamType, int channelId)
+Task StopRealPlay()
-void ThrowIfNotInited()
+Task TriggerInited(bool inited)
#override void OnParametersSet()
#override Task OnAfterRenderAsync(bool firstRender)
}
class BootstrapModuleComponentBase {
+string Id
+Dictionary~string, object?~ AdditionalAttributes
+ValueTask InvokeVoidAsync(string identifier, object? arg1, object? arg2, object? arg3, object? arg4, object? arg5, object? arg6)
+ValueTask<T> InvokeAsync<T>(string identifier, object? arg1, object? arg2, object? arg3, object? arg4, object? arg5, object? arg6)
}
class LoginType {
<<enum>>
Http
Https
Other
}
class HikVisionWebPlugin_js {
+Task init(string id, object invoke)
+Task login(string id, string ip, int port, string userName, string password, int loginType)
+Task logout(string id)
+Task startRealPlay(string id, int streamType, int channelId)
+Task stopRealPlay(string id)
}
class Hikvision_js {
+Task<bool> init(string id)
+Task<bool> login(string id, string ip, int port, string userName, string password, int loginType)
+Task logout(string id)
+Task<bool> startRealPlay(string id, int iStreamType, int iChannelID)
+Task<bool> stopRealPlay(string id)
+void dispose(string id)
}
HikVisionWebPlugin --|> BootstrapModuleComponentBase
HikVisionWebPlugin ..> LoginType
HikVisionWebPlugin ..> HikVisionWebPlugin_js
HikVisionWebPlugin_js ..> Hikvision_js
State diagram for HikVisionWebPlugin initialization, login, and real-play lifecyclestateDiagram-v2
[*] --> NotInited
NotInited --> Inited : TriggerInited(true)
NotInited --> InitFailed : TriggerInited(false)
Inited --> LoggedIn : Login success
Inited --> Inited : Login failure
LoggedIn --> Playing : StartRealPlay success
LoggedIn --> LoggedIn : StartRealPlay failure
Playing --> LoggedIn : StopRealPlay success
Playing --> Playing : StopRealPlay failure
LoggedIn --> Inited : Logout
Playing --> Inited : Logout
InitFailed --> [*]
Inited --> [*] : Dispose
LoggedIn --> [*] : Dispose
Playing --> [*] : Dispose
File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey there - I've reviewed your changes - here's some feedback:
- In
hikvision.js#dispose,Data.remove(id)is called beforestopRealPlay(id)/logout(id), but those functions rely onData.get(id)and destructuring, which will throw if the entry has already been removed; you should delayData.remove(id)until after these operations complete. - The new
OnInitedAsyncparameter onHikVisionWebPluginis declared as non-nullable but is used with a null check, so either mark it asFunc<bool, Task>?or provide a non-null default to keep nullability consistent. - Consider removing the
console.log(oWndInfo);debug statement instartRealPlayto avoid unnecessary console noise in production.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- In `hikvision.js#dispose`, `Data.remove(id)` is called before `stopRealPlay(id)`/`logout(id)`, but those functions rely on `Data.get(id)` and destructuring, which will throw if the entry has already been removed; you should delay `Data.remove(id)` until after these operations complete.
- The new `OnInitedAsync` parameter on `HikVisionWebPlugin` is declared as non-nullable but is used with a null check, so either mark it as `Func<bool, Task>?` or provide a non-null default to keep nullability consistent.
- Consider removing the `console.log(oWndInfo);` debug statement in `startRealPlay` to avoid unnecessary console noise in production.
## Individual Comments
### Comment 1
<location> `src/components/BootstrapBlazor.HikVision/wwwroot/hikvision.js:180` </location>
<code_context>
}
-export function logout(id) {
+export async function logout(id) {
const vision = Data.get(id);
- const { szDeviceIdentify } = vision;
+ const { szDeviceIdentify, logined } = vision;
+ if (logined !== true) {
+ vision.logined = false;
+ return;
+ }
- let completed = null;
- WebVideoCtrl.I_Logout(szDeviceIdentify).then(() => {
- completed = true;
- }, () => {
- completed = false;
- });
+ stopRealPlay(id);
- return new Promise((resolve, reject) => {
- const handler = setInterval(() => {
- if (completed !== null) {
- clearInterval(handler)
- resolve(vision);
- }
- }, 16);
- });
+ await WebVideoCtrl.I_Logout(szDeviceIdentify);
+ vision.logined = false;
}
</code_context>
<issue_to_address>
**suggestion (bug_risk):** `logout` ignores the asynchronous result of `stopRealPlay`, which may cause races.
`stopRealPlay(id)` now returns a `Promise`, but `logout` does not `await` it. This allows `logout` to continue (including clearing `logined`) before playback has actually stopped, and any rejection from `stopRealPlay` will be lost. If the goal is to stop playback before logout completes, `await stopRealPlay(id);` here and consider handling its boolean result.
```suggestion
await stopRealPlay(id);
```
</issue_to_address>
### Comment 2
<location> `src/components/BootstrapBlazor.HikVision/wwwroot/hikvision.js:186` </location>
<code_context>
}
-export async function startRealPlay(id) {
+export async function startRealPlay(id, iStreamType, iChannelID) {
const vision = Data.get(id);
const { iWndIndex, szDeviceIdentify } = vision;
</code_context>
<issue_to_address>
**issue (complexity):** Consider replacing the polling-based async handling in `startRealPlay`/`stopRealPlay` with direct Promise wrappers around the existing callbacks and returning a consistent Promise<boolean> API.
The new async handling in `startRealPlay` / `stopRealPlay` adds avoidable complexity via polling + shared flags and inconsistent return types. You can keep all current behavior while simplifying by directly wrapping the callback API in Promises and making the API consistently async.
### 1. Simplify `startRealPlay` async handling
You don’t need `completed` + `setInterval` because `I_StartRealPlay` already exposes `success`/`error` callbacks:
```js
export async function startRealPlay(id, iStreamType, iChannelID) {
const vision = Data.get(id);
const { iWndIndex, szDeviceIdentify } = vision;
vision.devicePort = await WebVideoCtrl.I_GetDevicePort(vision.szDeviceIdentify);
await getChannelInfo(vision);
const oWndInfo = WebVideoCtrl.I_GetWindowStatus(iWndIndex);
const iRtspPort = vision.devicePort.iRtspPort;
const bZeroChannel = false;
const startRealPlay = () => new Promise(resolve => {
WebVideoCtrl.I_StartRealPlay(szDeviceIdentify, {
iWndIndex,
iStreamType,
iChannelID,
bZeroChannel,
iPort: iRtspPort,
success: () => {
vision.realPlaying = true;
resolve(true);
},
error: () => {
vision.realPlaying = false;
resolve(false);
}
});
});
if (oWndInfo !== null) {
await new Promise(resolve => {
WebVideoCtrl.I_Stop({ success: resolve, error: resolve });
});
}
// preserve boolean success/failure result
return startRealPlay();
}
```
This keeps:
- `vision.realPlaying` updates
- boolean result indicating success/failure
- `I_Stop` before restart
but removes the polling loop and shared `completed` flag.
### 2. Make `stopRealPlay` consistently async and remove polling
Currently it sometimes returns `true` synchronously and sometimes a `Promise`, and it polls `completed`. You can keep behavior while returning a `Promise<boolean>` in all cases:
```js
export function stopRealPlay(id) {
const vision = Data.get(id);
const { iWndIndex, realPlaying } = vision;
if (realPlaying !== true) {
// already stopped
return Promise.resolve(true);
}
const oWndInfo = WebVideoCtrl.I_GetWindowStatus(iWndIndex);
if (oWndInfo === null) {
vision.realPlaying = false;
return Promise.resolve(true);
}
return new Promise(resolve => {
WebVideoCtrl.I_Stop({
success: () => {
vision.realPlaying = false;
resolve(true);
},
error: () => {
resolve(false);
}
});
});
}
```
This preserves:
- `vision.realPlaying` set to `false` on success
- boolean success/failure result
- “already stopped” fast-path
but removes the interval polling and type ambiguity (`boolean` vs `Promise<boolean>`).
If you want to keep `dispose` synchronous, you can still call `stopRealPlay(id)` without `await`; the above returns a Promise but callers that don’t care about completion can ignore it.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| }, () => { | ||
| completed = false; | ||
| }); | ||
| stopRealPlay(id); |
There was a problem hiding this comment.
suggestion (bug_risk): logout ignores the asynchronous result of stopRealPlay, which may cause races.
stopRealPlay(id) now returns a Promise, but logout does not await it. This allows logout to continue (including clearing logined) before playback has actually stopped, and any rejection from stopRealPlay will be lost. If the goal is to stop playback before logout completes, await stopRealPlay(id); here and consider handling its boolean result.
| stopRealPlay(id); | |
| await stopRealPlay(id); |
| } | ||
|
|
||
| export async function startRealPlay(id) { | ||
| export async function startRealPlay(id, iStreamType, iChannelID) { |
There was a problem hiding this comment.
issue (complexity): Consider replacing the polling-based async handling in startRealPlay/stopRealPlay with direct Promise wrappers around the existing callbacks and returning a consistent Promise API.
The new async handling in startRealPlay / stopRealPlay adds avoidable complexity via polling + shared flags and inconsistent return types. You can keep all current behavior while simplifying by directly wrapping the callback API in Promises and making the API consistently async.
1. Simplify startRealPlay async handling
You don’t need completed + setInterval because I_StartRealPlay already exposes success/error callbacks:
export async function startRealPlay(id, iStreamType, iChannelID) {
const vision = Data.get(id);
const { iWndIndex, szDeviceIdentify } = vision;
vision.devicePort = await WebVideoCtrl.I_GetDevicePort(vision.szDeviceIdentify);
await getChannelInfo(vision);
const oWndInfo = WebVideoCtrl.I_GetWindowStatus(iWndIndex);
const iRtspPort = vision.devicePort.iRtspPort;
const bZeroChannel = false;
const startRealPlay = () => new Promise(resolve => {
WebVideoCtrl.I_StartRealPlay(szDeviceIdentify, {
iWndIndex,
iStreamType,
iChannelID,
bZeroChannel,
iPort: iRtspPort,
success: () => {
vision.realPlaying = true;
resolve(true);
},
error: () => {
vision.realPlaying = false;
resolve(false);
}
});
});
if (oWndInfo !== null) {
await new Promise(resolve => {
WebVideoCtrl.I_Stop({ success: resolve, error: resolve });
});
}
// preserve boolean success/failure result
return startRealPlay();
}This keeps:
vision.realPlayingupdates- boolean result indicating success/failure
I_Stopbefore restart
but removes the polling loop and shared completed flag.
2. Make stopRealPlay consistently async and remove polling
Currently it sometimes returns true synchronously and sometimes a Promise, and it polls completed. You can keep behavior while returning a Promise<boolean> in all cases:
export function stopRealPlay(id) {
const vision = Data.get(id);
const { iWndIndex, realPlaying } = vision;
if (realPlaying !== true) {
// already stopped
return Promise.resolve(true);
}
const oWndInfo = WebVideoCtrl.I_GetWindowStatus(iWndIndex);
if (oWndInfo === null) {
vision.realPlaying = false;
return Promise.resolve(true);
}
return new Promise(resolve => {
WebVideoCtrl.I_Stop({
success: () => {
vision.realPlaying = false;
resolve(true);
},
error: () => {
resolve(false);
}
});
});
}This preserves:
vision.realPlayingset tofalseon success- boolean success/failure result
- “already stopped” fast-path
but removes the interval polling and type ambiguity (boolean vs Promise<boolean>).
If you want to keep dispose synchronous, you can still call stopRealPlay(id) without await; the above returns a Promise but callers that don’t care about completion can ignore it.
There was a problem hiding this comment.
Pull request overview
This PR adds an OnInitedAsync callback parameter to the HikVisionWebPlugin component, allowing consumers to be notified when the HikVision Web SDK plugin initialization completes. The changes improve state management by tracking initialization, login, and real-play states, and refactor several JavaScript functions to return promises with boolean results for better error handling.
Key Changes:
- Added
OnInitedAsynccallback parameter andInited,IsLogined,IsRealPlayingpublic properties to expose component state - Refactored JavaScript functions (
init,login,logout,startRealPlay,stopRealPlay) to return boolean values indicating success/failure - Added parameter validation in JavaScript and state checks in C# methods to prevent invalid operations
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
HikVisionWebPlugin.razor.cs |
Added OnInitedAsync callback parameter, state properties (Inited, IsLogined, IsRealPlaying), updated method signatures to return Task<bool>, and added validation logic |
HikVisionWebPlugin.razor.js |
Updated init function to accept invoke parameter and trigger the TriggerInited callback with initialization result |
hikvision.js |
Refactored multiple functions to return boolean success indicators, added state tracking (inited, logined, realPlaying), improved validation, and simplified promise handling |
HikVisionWebPlugin.razor |
Minor formatting change (whitespace consolidation) |
Comments suppressed due to low confidence (4)
src/components/BootstrapBlazor.HikVision/Components/HikVisionWebPlugin.razor.cs:165
- The documentation comment "触发 回调方法由 JavaScript 调用" is missing proper punctuation. It should be "触发 回调方法,由 JavaScript 调用" (add a comma after 回调方法).
src/components/BootstrapBlazor.HikVision/Components/HikVisionWebPlugin.razor.cs:132 - The
StartRealPlaymethod now requires two new parameters (streamTypeandchannelId), but the documentation does not describe them. Add XML documentation for the new parameters:
/// <param name="streamType">Stream type (e.g., main stream or sub stream)</param>
/// <param name="channelId">Channel ID to play</param>src/components/BootstrapBlazor.HikVision/Components/HikVisionWebPlugin.razor.cs:61
- The
OnInitedAsyncparameter should be nullable (Func<bool, Task>?) to match the pattern used consistently across the codebase. Other similar callback parameters in the codebase likeOnLockChangedCallbackAsyncin DockView,OnConnectin WebSerial, andOnIsBusyin WebSpeech all use nullable callback types.
src/components/BootstrapBlazor.HikVision/Components/HikVisionWebPlugin.razor.js:2 - Unused import EventHandler.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| }; | ||
|
|
||
| if (oWndInfo != null) { | ||
| console.log(oWndInfo); |
There was a problem hiding this comment.
This console.log statement should be removed before merging to production. Debug logging statements should not be left in production code.
| console.log(oWndInfo); |
| const { iWndIndex, realPlaying } = vision; | ||
|
|
||
| if (realPlaying !== true) { | ||
| return true; |
There was a problem hiding this comment.
The stopRealPlay function is not declared as async but returns a Promise. When the condition realPlaying !== true is met on line 241-243, it returns a boolean true directly instead of a Promise. This creates an inconsistent return type. Either make the function async and use return Promise.resolve(true) for the early return, or wrap the synchronous return in Promise.resolve(true).
| return true; | |
| return Promise.resolve(true); |
| } | ||
|
|
||
| return new Promise((resolve, reject) => { | ||
| const handler = setInterval(() => { | ||
| if (completed !== null) { | ||
| clearInterval(handler) | ||
| resolve(completed); | ||
| } | ||
| }, 16); | ||
| }); |
There was a problem hiding this comment.
If oWndInfo === null on line 247, the completed variable remains null and this Promise will never resolve, causing an infinite polling loop. Consider setting completed = true when oWndInfo === null to handle this case, or return early with a resolved value.
| } | |
| return new Promise((resolve, reject) => { | |
| const handler = setInterval(() => { | |
| if (completed !== null) { | |
| clearInterval(handler) | |
| resolve(completed); | |
| } | |
| }, 16); | |
| }); | |
| return new Promise((resolve, reject) => { | |
| const handler = setInterval(() => { | |
| if (completed !== null) { | |
| clearInterval(handler) | |
| resolve(completed); | |
| } | |
| }, 16); | |
| }); | |
| } else { | |
| // If oWndInfo is null, resolve immediately (not playing) | |
| return Promise.resolve(true); | |
| } |
| export function dispose(id) { | ||
| stopRealPlay(id); | ||
| logout(id); | ||
| const vision = Data.get(id); | ||
| Data.remove(id); | ||
|
|
||
| const { realPlaying, logined } = vision; | ||
| if (realPlaying === true) { | ||
| stopRealPlay(id); | ||
| } | ||
| if (logined === true) { | ||
| logout(id); | ||
| } | ||
| WebVideoCtrl.I_DestroyPlugin(); | ||
|
|
||
| Data.remove(id); | ||
| } |
There was a problem hiding this comment.
The dispose function calls Data.remove(id) on line 271 before using vision properties on lines 273-279. This could cause issues if other operations need the data. Additionally, stopRealPlay(id) and logout(id) are called after the data is removed, which may cause errors since they both call Data.get(id). Move Data.remove(id) to the end of the function, after all cleanup operations are complete.
| }, () => { | ||
| completed = false; | ||
| }); | ||
| stopRealPlay(id); |
There was a problem hiding this comment.
The stopRealPlay(id) call on line 180 is not awaited, but stopRealPlay returns a Promise. This could cause the logout to proceed before the real play is fully stopped. Add await before stopRealPlay(id) to ensure proper cleanup order.
| stopRealPlay(id); | |
| await stopRealPlay(id); |
Link issues
fixes #784
Summary By Copilot
Regression?
Risk
Verification
Packaging changes reviewed?
☑️ Self Check before Merge
Summary by Sourcery
Add a new HikVision web plugin component with initialization callback and richer state management for login and real-time preview, while tightening the JavaScript SDK’s lifecycle and status handling.
New Features:
Bug Fixes:
Enhancements: