plyr icon indicating copy to clipboard operation
plyr copied to clipboard

Pass headers to the HTTP reuqest

Open gp187 opened this issue 6 years ago • 14 comments

It would be a great feature to add basic authentication to the video source.

Passing the credentials directly in the link has been dropped by Chrome and more will follow so http://userb:[email protected]/elon_musk_riding_horse_backwards.mp4 won't pass the credentials any more.

const options = {
   headers: {
     'Authorization': 'Bearer iaf87h8fhd8idfidf=='
   },
   tooltips: {
    controls: true,
    seek: true
  },
 controls: ['play-large', 'play', 'progress', 'current-time', 'mute', 'volume', 'captions', 'settings', 'fullscreen']
};
this.videoPlayer = new Plyr(this.videoPlayerEl.nativeElement, options);

Passing a header to the HTTP request can open all sorts of posibilities for private video streaming

gp187 avatar Jan 11 '19 08:01 gp187

Does your example not achieve the same thing?

sampotts avatar Jan 11 '19 08:01 sampotts

headers: {
     'Authorization': 'Bearer iaf87h8fhd8idfidf=='
   },```

Headers property doesn't exist in the API. That was just my suggestion of who to do that

gp187 avatar Jan 12 '19 11:01 gp187

bumping this :)

dylmye avatar Dec 02 '19 12:12 dylmye

bump :)

Ashesh3 avatar Jun 01 '20 16:06 Ashesh3

bump

odeguycay avatar Jul 15 '20 10:07 odeguycay

bump

android040490 avatar Aug 06 '20 10:08 android040490

bump

codesiddhant avatar Aug 18 '20 22:08 codesiddhant

Bumps won't speed it up. Someone contributing will though. Use those key strokes for good. I don't think this will be a quick change as it will involve loading the content manually rather than using the browsers in-built logic which opens a whole other can of worms.

sampotts avatar Aug 18 '20 22:08 sampotts

Bumps won't speed it up. Someone contributing will though. Use those key strokes for good. I don't think this will be a quick change as it will involve loading the content manually rather than using the browsers in-built logic which opens a whole other can of worms.

Hey, I was able to solve this issue very easily using service workers, basically they act as a proxy and can inject custom headers.. even captures the requests required for playing the video.

Browser sends http requests to load the video (200 or 206) -> proxied by service worker's fetch handler -> add custom header -> Pass it normally without any efforts

Here's the service worker code i used to add an auth header:

File: ServiceWorker.js [register this in your main js file]

 self.addEventListener("fetch", event => {
        if (new URL(event.request.url).origin == "https://www.my_video_endpoint.com/") { //only add header to the endpoint i want
            event.respondWith(customHeaderRequestFetch(event));
        }
    });
    
    function customHeaderRequestFetch(event) {
        const newRequest = new Request(event.request, {
            mode: "cors",
            credentials: "omit",
            //also supports partial content (seeking)
            headers: { 
                range:
                    event.request.headers.get("range") != undefined
                        ? event.request.headers.get("range")
                        : "0-",
                Authorization:
                    "Bearer " +"xxxABCDEFGHxxx"
            }
        });
        return fetch(newRequest);
    }

That's the whole code, now just register this service worker, and let the browser and plyr do all the hard work (automatically) :)

Reference: https://developers.google.com/web/fundamentals/primers/service-workers

I would be happy to send a PR but i am not familiar with the project structure yet. Hope anyone can implement this!

Ashesh3 avatar Aug 20 '20 06:08 Ashesh3

Thanks @Ashesh3! https://github.com/sampotts/plyr/issues/1312#issuecomment-677267757 For anyone who using Angular:

  1. Create service worker JS file (e.g. sw.js), place it in some where (e.g. src/assets/js/service-worker/). File content is:
let domainInclude = ''; // Recieve from Component
let authToken = ''; // Recieve from Component

self.addEventListener('install', event => {
  const params = new URL(location);
  domainInclude = params.searchParams.get('include');
  authToken = params.searchParams.get('token');
  const installCompleted = Promise.resolve()
    .then(() => {});
  event.waitUntil(installCompleted);
});

self.addEventListener('activate', event => {
  event.waitUntil(
    self.clients.claim(),
    caches.keys().then((cacheNames) => {
        return Promise.all(
            cacheNames.map((cache) => {
                if (cache !== cacheName) {
                    return caches.delete(cache);
                }
            })
        );
    }));
});

// This function is implement from the guide of @Ashesh3
self.addEventListener('fetch', event => {
  if (event.request.url.includes(domainInclude)) {
    event.respondWith(customHeaderRequestFetch(event));
  }
});

function customHeaderRequestFetch(event) {
  const newRequest = new Request(event.request, {
    mode: "cors",
    credentials: "omit",
    //also supports partial content (seeking)
    headers: {
      range:
        event.request.headers.get("range") != undefined
          ? event.request.headers.get("range")
          : "0-",
      Authorization: authToken
    }
  });
  return fetch(newRequest);
}
  1. Edit file angular.json: Path: projects > <your-pj-name> > architect > build > options > assets:
"assets": [
   ..........
  {
      "glob": "**/*",
      "input": "src/assets/js/service-worker/",
      "output": "/"
   }
   ..........
]
  1. Go to your Component, where you want to start Service Worker:
ngOnInit() {
   if ('serviceWorker' in navigator) { // Make sure browser support Service Worker
      navigator.serviceWorker.register(`/sw.js?include=${encodeURIComponent('<URL_MATCHING>')}&token=${<YOUR_TOKEN>}`, {
        scope: '<SCOPE>'
      })
        .then(registration => registration.unregister());
        // SW unregistered still working till page reload, so don't worry.
        // But if you don't unregister here, Component may get stuck when you reload page,
        // because old SW is fetching video and not done yet.
        // In this case, If you stop playing video a while, page can be reload.
    } else {
      window.alert('Browser not support Service Worker.');
    }
}

Note:

  • <URL_MATCHING>: Send to sw.js, using to check matching the endpoint you need to add Headers. E.g. /api/video/: If Request URL include /api/video/ then sw.js will adding Headers to the request. (http://backend.local/api/video/aaaaa)
  • <YOUR_TOKEN>: Token send to sw.js for attach to Headers.
  • <SCOPE>: Router link where sw.js will working, e.g. /view-video/ (http://localhost:4200/view-video/aaaaa)

KangHidro avatar Sep 15 '20 13:09 KangHidro

Any solution only for html and javascript?

kevin4dhd avatar May 16 '21 16:05 kevin4dhd

if you are in streming using hls you can easily add header to request by this way

const hls = new Hls({ xhrSetup: xhr => { xhr.setRequestHeader('id', 1) xhr.setRequestHeader('token', 456) } })

Mersal-Mohamed avatar Nov 11 '21 10:11 Mersal-Mohamed

Please, i need to add Auhtorization Bearer {{token}} in the requests with Plyr. I'm using with React

import { useRef } from "react";
import Plyr from "plyr-react";
import "plyr-react/plyr.css";

interface VideoPlayerProps {
  sources: {
    src: string;
    size: number;
    type: string;
  }[];
  poster: string;
  token: string;
}

function VideoPlayer({ sources, poster, token }: VideoPlayerProps) {
  const playerRef = useRef(null);

  return (
    <Plyr
      ref={playerRef}
      options={{
        // headers: {
        //   Authorization: "Bearer iaf87h8fhd8idfidf==",
        // },
        tooltips: {
          controls: true,
        },
        controls: [
          "play-large",
          "play",
          "progress",
          "current-time",
          "mute",
          "volume",
          "captions",
          "settings",
          "fullscreen",
        ],
        settings: [
          "quality"
        ]
      }}
      source={{
        type: "video",
        sources: sources,
        poster: poster,
      }}
    />
  );
}

export default VideoPlayer;

devalefe avatar Apr 13 '23 19:04 devalefe

I've been able to do "private video streaming" (i.e. video files behind HTTP auth on a separate domain) by first making a simple request via XMLHttpRequest to the server. It can be for anything, so I just request a small icon file that's hidden behind auth. The user then gets asked for credentials by the browser since I don't send the authorization token with the request. After that, videos behind HTTP auth play with Plyr just fine, no crossOrigin needed even if it's on a separate domain. You just need to ensure CORS headers are set up correctly on the other server, of course.

For subtitle tracks, I download the subs through an auth'd XMLHttpRequest, create a blob url for it, and pass the blob url to Plyr. The XMLHttpRequest therefore takes care of any authentication and cross-domain stuff for subs, as the blob url is not cross-domain1.

While this works for me, I have not tried passing the token directly in the initial "small icon" request prior to loading the video resource through Plyr, so I do not know if the browser would "hold onto" that token when making the video request2.

Here's a small sample of how I do it (I'm using React), abbreviated heavily for brevity:

// Step 1. Load some auth'd resource with username/password prompt

let xhr = new XMLHttpRequest();
xhr.withCredentials = true;     // This will ask you for credentials first time
xhr.open('GET', 'https://example.com/protected/icon.ico');
xhr.send(); // discard the icon data

// Step 2. Load subtitle file

let subreq = new XMLHttpRequest();
subreq.withCredentials = true;  // You already auth'd in step 1, so no prompt this time.
                                // You may not even need this line, but in my code
                                // I have request stuff in a function that includes it.
subreq.open('GET', 'https://example.com/protected/subs.vtt');
subreq.send();

const bloburl = URL.createObjectURL(
    new Blob([subreq.response], {
        type: 'text/vtt',
    })
);

// Step 3. Load Plyr. The auth in "Step 1" gets applied to Plyr.

<Plyr
    source={{
        type: "video",
        sources: [{
            src: 'https://example.com/protected/video.mp4',
            type: "video/mp4",
        }],
        tracks: [{
            kind: 'captions',
            label: 'english',
            srcLang: 'en',
            src: bloburl,
        }],
    }}
/>

I hope that helps somebody, and sorry if it was confusing/too long. I'm not good at explaining things concisely. I'm also very new to web development, so it's completely possible I'm doing things in a not-so-smart way here. I just know it seems to work consistently for me.

tl;dr Videos behind auth are loaded normally if you make a prior request (e.g. XMLHttpRequest) that requires auth2. Subtitle tracks can be downloaded via a manual XMLHttpRequest and passed to Plyr via blob url3.

1,3 My subtitles (and videos) are on a separate domain, so I don't know if just placing the URL for same-domain subtitles would also automatically auth like the videos do. However, it seems off-domain subs do not load directly via Plyr without crossOrigin set, but setting as crossOrigin="anonymous" prevented video loading and threw a 401 Unauthorized for the subtitles. Maybe it would work if I set it to the player site's domain, but I did not try that since using a blob works so well.

2 I make the user manually input username/password, so no auth token is directly set/sent in my code. I don't know if sending the token directly over XMLHttpRequest will yield the same behavior. I've only tested using XMLHttpRequest.

buhao8 avatar Jul 11 '24 04:07 buhao8