Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
@namespace BootstrapBlazor.Components
@inherits BootstrapModuleComponentBase

<div @attributes="AdditionalAttributes" class="@ClassString" id="@Id" style="@StyleString">
</div>
<div @attributes="AdditionalAttributes" class="@ClassString" id="@Id" style="@StyleString"></div>
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
namespace BootstrapBlazor.Components;

/// <summary>
/// 海康威视网络摄像机组件
/// 海康威视网络摄像机组件 (Websdk Plugin 插件版本)
/// </summary>
[JSModuleAutoLoader("./_content/BootstrapBlazor.HikVision/Components/HikVision.razor.js")]
public partial class HikVision
[JSModuleAutoLoader("./_content/BootstrapBlazor.HikVision/Components/HikVisionWebPlugin.razor.js", JSObjectReference = true)]
public partial class HikVisionWebPlugin
{
/// <summary>
/// 获得/设置 网络摄像机 IP 地址
Expand Down Expand Up @@ -54,6 +54,12 @@ public partial class HikVision
[Parameter]
public string? Height { get; set; }

/// <summary>
/// 获得/设置 插件初始化完成后回调方法
/// </summary>
[Parameter]
public Func<bool, Task> OnInitedAsync { get; set; }

private string? ClassString => CssBuilder.Default("bb-hik")
.AddClassFromAttributes(AdditionalAttributes)
.Build();
Expand All @@ -64,6 +70,21 @@ public partial class HikVision
.AddStyleFromAttributes(AdditionalAttributes)
.Build();

/// <summary>
/// 获得 Websdk 插件是否初始化成功
/// </summary>
public bool Inited { get; private set; }

/// <summary>
/// 获得 是否已登录
/// </summary>
public bool IsLogined { get; private set; }

/// <summary>
/// 获得 是否正在实时预览
/// </summary>
public bool IsRealPlaying { get; private set; }

/// <summary>
/// <inheritdoc/>
/// </summary>
Expand All @@ -84,9 +105,11 @@ protected override void OnParametersSet()
/// <param name="password"></param>
/// <param name="loginType"></param>
/// <returns></returns>
public async Task Login(string ip, int port, string userName, string password, LoginType loginType = LoginType.Http)
public async Task<bool> Login(string ip, int port, string userName, string password, LoginType loginType = LoginType.Http)
{
await InvokeVoidAsync("login", Id, ip, port, userName, password, (int)loginType);
ThrowIfNotInited();
IsLogined = await InvokeAsync<bool?>("login", Id, ip, port, userName, password, (int)loginType) ?? false;
return IsLogined;
}

/// <summary>
Expand All @@ -95,16 +118,23 @@ public async Task Login(string ip, int port, string userName, string password, L
/// <returns></returns>
public async Task Logout()
{
await InvokeVoidAsync("logout", Id);
if (IsLogined)
{
await InvokeVoidAsync("logout", Id);
}
IsLogined = false;
}

/// <summary>
/// 开始实时预览方法
/// </summary>
/// <returns></returns>
public async Task StartRealPlay()
public async Task StartRealPlay(int streamType, int channelId)
{
await InvokeVoidAsync("startRealPlay", Id);
if (IsLogined && !IsRealPlaying)
{
IsRealPlaying = await InvokeAsync<bool?>("startRealPlay", Id, streamType, channelId) ?? false;
}
}

/// <summary>
Expand All @@ -113,6 +143,36 @@ public async Task StartRealPlay()
/// <returns></returns>
public async Task StopRealPlay()
{
await InvokeVoidAsync("stopRealPlay", Id);
if (IsLogined && IsRealPlaying)
{
var result = await InvokeAsync<bool?>("stopRealPlay", Id) ?? false;
if (result)
{
IsRealPlaying = false;
}
}
}

private void ThrowIfNotInited()
{
if (!Inited)
{
throw new InvalidOperationException("HikVision Web Plugin not inited");
}
}

/// <summary>
/// 触发 <see cref="OnInitedAsync"/> 回调方法由 JavaScript 调用
/// </summary>
/// <returns></returns>
[JSInvokable]
public async Task TriggerInited(bool inited)
{
Inited = inited;

if (OnInitedAsync != null)
{
await OnInitedAsync(inited);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { init as initVision, login, logout, startRealPlay, stopRealPlay, dispose as disposeVision } from '../hikvision.js';
import EventHandler from '../../BootstrapBlazor/modules/event-handler.js';

export async function init(id) {
export async function init(id, invoke) {
const el = document.getElementById(id);
if (el === null) {
return;
}

await initVision(id);
const inited = await initVision(id);
await invoke.invokeMethodAsync('TriggerInited', inited);
}

export { login, logout, startRealPlay, stopRealPlay }
Expand Down
113 changes: 75 additions & 38 deletions src/components/BootstrapBlazor.HikVision/wwwroot/hikvision.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,20 @@ export async function init(id) {

const el = document.getElementById(id);
if (el === null) {
return;
return false;
}

const result = await initWindow(id);
if (result.inited === false) {
return;
return false;
}

Data.set(id, {
iWndIndex: result.iWndIndex
iWndIndex: result.iWndIndex,
inited: true
});

return true;
}

const initWindow = id => {
Expand All @@ -30,7 +33,7 @@ const initWindow = id => {
bWndFull: true,
iWndowType: 1,
cbSelWnd: function (xmlDoc) {
result.iWndIndex = getTagNameFirstValue(xmlDoc, "SelectWnd")
result.iWndIndex = parseInt(getTagNameFirstValue(xmlDoc, "SelectWnd"));
},
cbDoubleClickWnd: function (iWndIndex, bFullScreen) {

Expand Down Expand Up @@ -59,6 +62,14 @@ const initWindow = id => {

export async function login(id, ip, port, userName, password, loginType) {
const vision = Data.get(id);
const { inited, logined } = vision;
if (inited !== true || ip.length === 0 || port <= 0 || userName.length === 0 || password.length === 0) {
return false;
}
if (logined === true) {
return true;
}

vision.szDeviceIdentify = `${ip}_${port}`;
vision.logined = null;
vision.loginErrorCode = null;
Expand All @@ -84,9 +95,9 @@ export async function login(id, ip, port, userName, password, loginType) {

return new Promise((resolve, reject) => {
const handler = setInterval(async () => {
if (vision.logined !== void 0) {
if (vision.logined !== null) {
clearInterval(handler)
resolve(vision);
resolve(vision.logined);
}
}, 16);
});
Expand Down Expand Up @@ -152,34 +163,27 @@ const getChannelInfo = vision => {
const handler = setInterval(() => {
if (analog_completed && digital_completed && zero_completed) {
clearInterval(handler)
resolve(vision);
resolve();
}
}, 16);
});
}

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);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
stopRealPlay(id);
await stopRealPlay(id);

Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
stopRealPlay(id);
await stopRealPlay(id);

Copilot uses AI. Check for mistakes.

return new Promise((resolve, reject) => {
const handler = setInterval(() => {
if (completed !== null) {
clearInterval(handler)
resolve(vision);
}
}, 16);
});
await WebVideoCtrl.I_Logout(szDeviceIdentify);
vision.logined = false;
}

export async function startRealPlay(id) {
export async function startRealPlay(id, iStreamType, iChannelID) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.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:

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.

const vision = Data.get(id);
const { iWndIndex, szDeviceIdentify } = vision;

Expand All @@ -188,26 +192,28 @@ export async function startRealPlay(id) {

const oWndInfo = WebVideoCtrl.I_GetWindowStatus(iWndIndex);
const iRtspPort = vision.devicePort.iRtspPort;
const iChannelID = 1;
const bZeroChannel = false;
const iStreamType = 1;

let completed = null;
const startRealPlay = function () {
WebVideoCtrl.I_StartRealPlay(szDeviceIdentify, {
iWndIndex: iWndIndex,
iStreamType: iStreamType,
iChannelID: iChannelID,
bZeroChannel: bZeroChannel,
iPort: iRtspPort,
success: function () {

vision.realPlaying = true;
completed = true;
},
error: function (oError) {

vision.realPlaying = false;
completed = false;
}
});
};

if (oWndInfo != null) {
console.log(oWndInfo);
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This console.log statement should be removed before merging to production. Debug logging statements should not be left in production code.

Suggested change
console.log(oWndInfo);

Copilot uses AI. Check for mistakes.
if (oWndInfo !== null) {
WebVideoCtrl.I_Stop({
success: function () {
startRealPlay();
Expand All @@ -217,39 +223,70 @@ export async function startRealPlay(id) {
else {
startRealPlay();
}

return new Promise((resolve, reject) => {
const handler = setInterval(() => {
if (completed !== null) {
clearInterval(handler)
resolve(completed);
}
}, 16);
});
}

export function stopRealPlay(id) {
const vision = Data.get(id);
const { iWndIndex, szDeviceIdentify } = vision;
const { iWndIndex, realPlaying } = vision;

if (realPlaying !== true) {
return true;
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
return true;
return Promise.resolve(true);

Copilot uses AI. Check for mistakes.
}

const oWndInfo = WebVideoCtrl.I_GetWindowStatus(iWndIndex);
let completed = null;
if (oWndInfo !== null) {
WebVideoCtrl.I_Stop({
success: function () {

vision.realPlaying = false;
completed = true;
},
error: function (oError) {

completed = false;
}
});
}

return new Promise((resolve, reject) => {
const handler = setInterval(() => {
if (completed !== null) {
clearInterval(handler)
resolve(completed);
}
}, 16);
});
Comment on lines 257 to +266
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
}
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);
}

Copilot uses AI. Check for mistakes.
}

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);
}
Comment on lines 269 to 282
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.

const getTagNameFirstValue = (xmlDoc, tagName) => {
const getTagNameFirstValue = (xmlDoc, tagName, defaultValue = '0') => {
const tags = xmlDoc.getElementsByTagName(tagName);
if (tags.length > 0) {
return tags[0].textContent;
}
return null;
return defaultValue;
}

const getTagNameValues = (xmlDoc, tagName) => {
Expand Down
Loading