Search code examples
javascriptc#angularjwtvideo-streaming

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!


Solution

  • 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