Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Stream video from a secured endpoint using Angular

I have secured endpoint. I need to pass a jwt token in the head of a Http Get request from Angular to stream the video.

The endpoint in a dotnet core controller looks like this (simplified):

[Route("api/GetVideo/{videoId}")]
public async Task<IActionResult> GetVideoAsync(int videoId)
{
    string videoFilePath = GiveMeTheVideoFilePath(videoId);  

    return this.PhysicalFile(videoFilePath, "application/octet-stream", enableRangeProcessing: true);
}

Angular code: video.component.html

<video width="100%" height="320" crossorigin="anonymous" controls #videoElement>
        <source
            [src]="'http://mydomain/api/GetVideo/1' | authVideo | async"  type="application/octet-stream"
        />
</video>

video.pipe.ts

@Pipe({
    name: 'authVideo',
})
export class AuthVideoPipe implements PipeTransform {
    constructor(private http: HttpClient, private auth: AuthTokenService) {}

    transform(url: string) {
        return new Observable<string>(observer => {
            const token = this.auth.getToken(); //this line gets the jwt token
            const headers = new HttpHeaders({ Authorization: `Bearer ${token}` });

            const { next, error } = observer;

            return this.http.get(url, { headers, responseType: 'blob' }).subscribe(response => {
                const reader = new FileReader();
                reader.readAsDataURL(response);
                reader.onloadend = function() {
                    observer.next(reader.result as string);
                };
            });
        });
    }
}

It does make a get request to the endpoint with the above code. And something is returned to the front-end. But the video is not playing. I found the above way from this SO question. It works for images, but that doesn't work for video apparently. My thought is that I might need to read the streams byte by byte in the front-end. If so, how do I do that?

I have tried changing "application/octet-stream" to "video/mp4" on both ends. But no luck.

Note that when I removed security code from the back-end, and removed the authVideo pipe from the html it works perfectly. Please shed me some light. Thank you!

like image 689
Lok Avatar asked Aug 01 '20 00:08

Lok


2 Answers

While this solution works for images because all of the data is loaded once as a data URL, you shouldn't be doing this for a video as it disables the streaming ability of the browser. Indeed by doing this you are loading the entire video in memory before transforming it into a data URL, which is really bad in terms of performance and user experience (requires the full video to be loaded before playing it, and causes heavy memory consumption).

The obvious solution to your problem is to use cookies for authentication:

  • A cookie is set when the authentication succeeds
  • It is automatically sent back in subsequent requests, including the video one

In the following I'm assuming you can't do that for some reason.

You could use MediaSource. It allows you to control the actual request that is sent to retrieve the video (and add the Authorization header). Note that even if this is widely supported by all of the browsers, this is experimental.

Your code should look like this:

assetURL = 'http://mydomain/api/GetVideo/1';

// Modify this with the actual mime type and codec
mimeCodec = 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"';

@ViewChild("videoElement") video: ElementRef;

ngAfterViewInit() {
  if (
    "MediaSource" in window &&
    MediaSource.isTypeSupported(this.mimeCodec)
  ) {
    const mediaSource = new MediaSource();
    (this.video.nativeElement as HTMLVideoElement).src = URL.createObjectURL(
      mediaSource
    );
    mediaSource.addEventListener("sourceopen", () =>
      this.sourceOpen(mediaSource)
    );
  } else {
    console.error("Unsupported MIME type or codec: ", this.mimeCodec);
  }
}

sourceOpen(mediaSource) {
  const sourceBuffer = mediaSource.addSourceBuffer(this.mimeCodec);
  const token = this.auth.getToken(); //this line gets the jwt token
  const headers = new HttpHeaders({ Authorization: `Bearer ${token}` });
  return this.http
    .get(this.assetURL, { headers, responseType: "blob" })
    .subscribe(blob => {
      sourceBuffer.addEventListener("updateend", () => {
        mediaSource.endOfStream();
        this.video.nativeElement.play();
      });
      blob.arrayBuffer().then(x => sourceBuffer.appendBuffer(x));
    });
}

This working demo gives the following result:

Request headers

like image 138
Guerric P Avatar answered Oct 19 '22 23:10

Guerric P


In my case, the URI of the webApi is the same as the main application, which is gaurded by an authentication service. The flow basically works like this: Video Streaming

Before a request has reached the webApi endpoint, the authenticationService will determine whether the user is allowed to view this endpoint. e.g. whether the user is logged in, does the user have permission to view this endpoint, etc.

If a request has come to the webApi controller, the rest is pretty simple. It can just call the method in the videoService that returns the video stream.

In the frontend, all you need is this(of course you can make the videoId dynamic):

<video width="100%" height="320" controls #videoElement>
    <source
        [src]="'http://mydomain/api/GetVideo/1' | async"
    />
</video>

The back will look like this:

[Route("api")]
public class WebApiController : Controller
{
    [HttpGet("GetVideo/{videoId}")]
    public async Task<IActionResult> GetVideoAsync(int videoId)
    {
        // Note: videoService can be on the same server, or a service
        // on a different server depends on how you implement it
        return this.videoService.GetVideoStream(videoId);
    }
}
like image 21
Lok Avatar answered Oct 19 '22 23:10

Lok