4

I'm starting to work with Blazor. My intention is playing one random Video after another. Therefore i wanted to use the Blazor Event Listener. But the onended Event is not firing (everything is working fine with onclick).

video Element:

<figure id="video-player">
    <video autoplay="autoplay" @onended="NewVideo">
        @videoUrl
    </video>
</figure>

Codeblock:

private MarkupString videoUrl;

    protected override void OnInitialized()
    {
        NewVideo();
    }

    private void NewVideo()
    {
        videoUrl = new MarkupString($"<source src=\"videos/{tv.GetRandomVideoFileName()}\" type=\"video/mp4\">");

    }

OnInitialized is working as intended, and if I change the onended to onclick everything is also working fine.

To Mention: I know, only changing the source wouldnt start the next Video. That would be my next Task on the List :). First I only want to Change the source in the DOM.

SvenMind
  • 41
  • 3
  • have you tried removing the `autoplay` attribute? it might be the cause. – muratgu Jul 25 '20 at 23:25
  • Tried it now, didn't helped. I already tried it the tradition way with Javascript, which worked. But I want to get it work with Blazor :( – SvenMind Jul 25 '20 at 23:33
  • 1
    I believe this is because video playback events do not bubble - and Blazor registers all event handlers at the document level. You will probably need to register JS event handlers that callback to .NET instead. – Mister Magoo Jul 26 '20 at 02:07
  • That truly could be the Issue. For me this looks like a Bug at Blazor (there shouldn't be an @onended Event, mentioned in the [Docu](https://learn.microsoft.com/de-de/aspnet/core/blazor/components/event-handling?view=aspnetcore-3.1)). So I created an [Issue on Github](https://github.com/dotnet/aspnetcore/issues/24323) – SvenMind Jul 26 '20 at 07:45

2 Answers2

2

The short story is that this isn't supported, and did not make the v5 release. See the issue here:

https://github.com/dotnet/aspnetcore/issues/24323

The longer story is that you'll have to fire up some JS interop. Reference is here: https://learn.microsoft.com/en-us/aspnet/core/blazor/call-dotnet-from-javascript?view=aspnetcore-5.0#component-instance-method-call

On the JS side, you'll need something like this:

var player = document.getElementById('player');
player.onended = (e) => {
    DotNet.invokeMethodAsync('{assembly_name_here}', 'SongEnded');
};

The assembly name is probably the name of the project, like MyBlazorApp. Then in your component, you'll need a static action to get the thing wired up:

    protected override void OnInitialized()
    {
        action = UpdateMessage;
    }
    private static Action action;

    private void UpdateMessage()
    {
        // DO STUFF HERE
    }

    [JSInvokable]
    public static void SongEnded()
    {
        action.Invoke();
    }

It's a little clunky, sure, but essentially you're mapping a JS event to a static method on the component. You're probably thinking, "Well, what if I have multiple instances of the component?" In that case, you'll have to be a little creative about passing context to the static method and finding the right instance. Again, check the reference above, it has plenty of examples.

Jeff Putz
  • 14,504
  • 11
  • 41
  • 52
0

Came up with a solution to avoid static method. In code part we need to hold reference of DotNetObjectReference and also some index of it (that will play part in JS), this is example class:

public abstract class VideoPlayerBase : ComponentBase, IDisposable
{
    [Parameter] public string Source { get; set; }
    [Parameter] public EventCallback VideoEndedCallback { get; set; }

    [Inject] protected IJSRuntime JS { get; set; }

    /// <summary>
    /// DotNetObjectReference of current component instance.
    /// This is used for catching `onended` event on video tag (which apparently is not supported by Blazor)
    /// </summary>
    private DotNetObjectReference<VideoPlayerBase>? _dotNetObjectReference;

    /// <summary>
    /// Index of DotNetObjectReference inside BlazorApp.dotNetObjectReferences array.
    /// This is used to be able to relevant DotNetObjectReference from PriskApp.dotNetObjectReferences array.
    /// </summary>
    protected int DotNetObjectReferenceIndex { get; set; } = -1;

    protected override async Task OnInitializedAsync()
    {
        await base.OnInitializedAsync();
        _dotNetObjectReference = DotNetObjectReference.Create(this);
        DotNetObjectReferenceIndex = await JS.InvokeAsync<int>("BlazorApp.addDotNetObjectReference", _dotNetObjectReference);
    }

    [JSInvokable("VideoEnded")]
    public async Task VideoEndedAsync()
    {
        await VideoEndedCallback.InvokeAsync(null);
    }

    public void Dispose()
    {
        _dotNetObjectReference?.Dispose();
    }
}

Then we need js part:

var BlazorApp = BlazorApp || {
    dotNetObjectReferences: [],

    addDotNetObjectReference: function (dotNetObjectReference) {
        PriskApp.dotNetObjectReferences.push(dotNetObjectReference);
        return PriskApp.dotNetObjectReferences.length - 1;
    },

    getDotNetObjectReference: function (index) {
        return PriskApp.dotNetObjectReferences[index];
    },
};

And lastly the component view:

@inherits VideoPlayerBase

<div>
    <video style="width:100%;" controls onended="(BlazorApp.getDotNetObjectReference(@DotNetObjectReferenceIndex).invokeMethodAsync('VideoEnded'))()">
        <source src="@Source" type="video/mp4" controls>
        Your browser does not support HTML video.
    </video>
</div>

So basically this work by creating DotNetObjectReference in OnInitializedAsync method and then calling js function BlazorApp.addDotNetObjectReference to add this reference to some js array and to get index of it, which is used in view part. Then once Video ends onended event is triggered and inside it it gets the relevant DotNetObjectReference and calls its method.

Grengas
  • 836
  • 12
  • 16