sampotts / plyr

A simple HTML5, YouTube and Vimeo player
https://plyr.io
MIT License
26.44k stars 2.92k forks source link

double tap to fast forward #2156

Open AnasProgrammer2 opened 3 years ago

AnasProgrammer2 commented 3 years ago

hello , please update player with feature ( double tap to fast forward +5 Sec or more )

amaank404 commented 3 years ago

well coincidently i was going to write a issue like this but found this one. so i am in favor for this feature

AnasProgrammer2 commented 3 years ago

well coincidently i was going to write a issue like this but found this one. so i am in favor for this feature

thanks ,

AnasProgrammer2 commented 3 years ago

well coincidently i was going to write a issue like this but found this one. so i am in favor for this feature

waiting your update

AnasProgrammer2 commented 3 years ago

Can add just buttons to forward 10sec?

amaank404 commented 3 years ago

Can add just buttons to forward 10sec?

that might also be great, let's wait for a dev/maintainer/owner of this project to view it. All the best to awesome devs who built this player 😄

chrisbbreuer commented 3 years ago

I personally like this feature as well. Attached are how Facebook and Twitter handle this on mobile devices.

Interesting to point out, and to keep in mind, this behavior is only implemented for touch-screen/mobile devices. It does not exist in the desktop experience on either platform.

Here are 3 screenshots of how Twitter and Facebook handle it:

amaank404 commented 3 years ago

i suggest if not double click, then the facebook type buttons could be added after checking if the device is a mobile, tablet device or a laptop, PC

denis-mironov commented 3 years ago

Any news about this feature?

amaank404 commented 3 years ago

No news as of now, although I have managed to inject my custom buttons using some code after the player loads. Screenshot_20210901_190802 I use brython in my projects so you can translate this into javascript if needed. Icons are svg images from google's icon set. Here is the code used:

<script type="text/javascript">
            function insertAfter(newNode, existingNode) {
                existingNode.parentNode.insertBefore(newNode, existingNode.nextSibling);
            }
        </script>
<script type="text/python">
            from browser import document, window, html, bind, timer

            def wait_and_init():
                elm = document.select(".plyr__controls__item")[0]
                forward_elm = html.IMG("", **{"src": "{{ url_for('static', filename='static/images/forward_10_white.svg') }}",
                                              "class": "qcontrols",
                                              "id": "forward-10"})
                replay_elm = html.IMG("", **{"src": "{{ url_for('static', filename='static/images/replay_10_white.svg') }}",
                                              "class": "qcontrols",
                                              "id": "replay-10"})
                window.insertAfter(forward_elm, elm)
                window.insertAfter(replay_elm, elm)

                @bind(replay_elm, "click")
                def replay_10_seconds(evt):
                    window.player.rewind(10)

                @bind(forward_elm, "click")
                def forward_10_seconds(evt):
                    window.player.forward(10)

            timer.set_timeout(wait_and_init, 1000)

        </script>
amaank404 commented 3 years ago

Interestingly, I have managed to build the double click feature from this page but with some modifications to make it look just a little better. Here is the scripting part and html part (all in javascript):

First add the following css to your website:

<style type="text/css">
        .player {
  width:100%;
  border: 5px solid rgba(0,0,0,0.2);
  box-shadow: 0 0 20px rgba(0,0,0,0.2);
  position: relative;
  font-size: 0;
  overflow: hidden;
}

video{
  width:100%;
  display:block;
}
.video-container{
    position: relative;
    overflow: hidden;
}
.video-forward-notify{
  text-align: center;
  width:30%;
  height:200%;
  border-radius:100% 0 0 100%;
  position: absolute;
  display:flex;
  flex-direction: row;
  right: 0%;
  top:-50%;
}

.video-forward-notify .icon{
  justify-content:flex-start;
  align-items:center;
  margin: auto 0 auto 40%;
  color: white;
}
.video-rewind-notify{
  text-align: center;
  width:30%;
  height:200%;
  border-radius:0 100% 100% 0;
  position: absolute;
  display:flex;
  flex-direction: row;
  left: 0;
  top:-50%;
}

.video-rewind-notify .icon{
  justify-content:flex-start;
  align-items:center;
  margin: auto 0 auto 40%;
  color: white;
}
.icon i{
  display:block;
}
.notification{
  transition: background 0.8s;
  background: rgba(200,200,200,.4) radial-gradient(circle, transparent 1%, rgba(200,200,200,.4) 1%) center/15000%;
  pointer-events:none;
  display: none;
}
i{
  font-style:normal;
}
.animate-in{
  display:flex;
  animation: ripple 1s forwards;
}
.animate-in i{
  display:block;
}
.animate-in.forward i{
  padding-bottom:2px;
}
.animate-in.forward i{
  animation: fadeInLeft .7s;
}
.animate-in.rewind i{
  animation: fadeInRight .7s;
}
@keyframes ripple{
  0%   { 
    background-color: rgba(200,200,200,.4);
    background-size: 100%;
    transition: background 0s;
    opacity:1;
  }
  100% { 
  transition: background 0.8s;
  background: rgba(200,200,200,.4) radial-gradient(circle, transparent 1%, rgba(200,200,200,.4) 1%) center/15000%;
  display: flex;
    opacity:0;
  }
}
@keyframes fadeInLeft {
  0% {
    opacity: 0;
    transform: translateX(-20px);
  }
  100% {
    opacity: 1;
    transform: translateX(0);
  }
}
@keyframes fadeInRight {
  0% {
    opacity: 0;
    transform: translateX(0px);
  }
  100% {
    opacity: 1;
    transform: translateX(-20px);
  }
}
font12{
  font-size:12px;
}
</style>

Now, do the following with the video tag that is going to be initialised with plyr:

<div class="video-container">
            <video controls playsinline autoplay id="video_player">
            </video>
            <div class="video-rewind-notify rewind notification">
                <div class="rewind-icon icon">
                    <i class="left-triangle triangle">◀◀◀</i>
                    <span class="rewind font12">10 seconds</span>
                </div>
              </div>
              <div class="video-forward-notify forward notification">
                <div class="forward-icon icon">
                    <i class="right-triangle triangle">▶▶▶</i>
                    <span class="forward font12">10 seconds</span>
                </div>
              </div>
        </div>

With the above done for video tag replacement add the following scripts to the end of body tag:

<script type="text/javascript">

//grab the video dom element
const video = document.querySelector('video'); 
const notifications = document.querySelectorAll('.notification');
const forwardNotificationValue = document.querySelector('.video-forward-notify span');
const rewindNotificationValue = document.querySelector('.video-rewind-notify span');

let timer;
let rewindSpeed = 0;
let forwardSpeed = 0;

//function for double click event listener on the video
//todo change those variable to html5 data attributes
function updateCurrentTime(delta){
    let isRewinding = delta < 0;

    if(isRewinding){
      rewindSpeed = rewindSpeed + delta;
      forwardSpeed = 0;
    }else{
      forwardSpeed = forwardSpeed + delta;
      rewindSpeed = 0;
    }

    //clear the timeout
    clearTimeout(timer);

    let speed = (isRewinding ? rewindSpeed : forwardSpeed);
    video.currentTime = video.currentTime + speed;

    let NotificationValue =  isRewinding ? rewindNotificationValue : forwardNotificationValue ;
    NotificationValue.innerHTML = `${Math.abs(speed)} seconds`;

    //reset accumulator within 2 seconds of a double click
    timer = setTimeout(function(){
      rewindSpeed = 0;
      forwardSpeed = 0;
    }, 2000); // you can edit this delay value for the timeout, i have it set for 2 seconds
    console.log(`updated time: ${video.currentTime}`);
}

function animateNotificationIn(isRewinding){
  isRewinding ? notifications[0].classList.add('animate-in') : notifications[1].classList.add('animate-in'); 
}

function animateNotificationOut(){
    this.classList.remove('animate-in');
}

function forwardVideo(){
  updateCurrentTime(10);
  animateNotificationIn(false);
}

function rewindVideo(){
    updateCurrentTime(-10);
    animateNotificationIn(true);
}

//Event Handlers
function doubleClickHandler(e){
    console.log(`current time: ${video.currentTime}`);
    const videoWidth = video.offsetWidth;
    (e.offsetX < videoWidth/2) ? rewindVideo() : forwardVideo();
}

function togglePlay(){
  video.paused ? video.play() : video.pause();
}

// If you want it to work on desktop browsers, just replace the condition with true
if (window.is_tablet_browser() || window.is_mobile_browser()) {
  //Event Listeners
  video.addEventListener('click', togglePlay);
  video.addEventListener('dblclick', doubleClickHandler);
  notifications.forEach(function(notification){
    notification.addEventListener('animationend', animateNotificationOut);
  });
}
        </script>

Make sure to add this utility script tag somewhere in the head tag:

<script type="text/javascript">
function is_mobile_browser () {
    let check = false;
    (function(a){if(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(a)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0,4))) check = true;})(navigator.userAgent||navigator.vendor||window.opera);
    return check;
}

function is_tablet_browser () {
    let check = false;
    (function(a){if(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino|android|ipad|playbook|silk/i.test(a)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0,4))) check = true;})(navigator.userAgent||navigator.vendor||window.opera);
    return check;
}
</script>

Now remove the doubleclick event for plyr to fullscreen so it does not interfere with our double click event to forward or rewind. Add the following just after initializing plyr:

player.eventListeners.forEach(function(eventListener) {
    if(eventListener.type === 'dblclick') {
        eventListener.element.removeEventListener(eventListener.type, eventListener.callback, eventListener.options);
    };
});

With the above modifications made. When your website is opened in a tablet or a mobile browser, the double tap to forward or rewind will be available. Note: This can work on desktop and laptops but it's disabled by default in the script. You can modify the provided script to make it work on desktops also

RaSan147 commented 2 years ago

Okk I've done it with some css and js Limitations, one video per page (you can twick the code and by pass that

const byId = document.getElementById.bind(document),
    byClass = document.getElementsByClassName.bind(document),
    byTag = document.getElementsByTagName.bind(document),
    byName = document.getElementsByName.bind(document),
    createElement = document.createElement.bind(document);

var player = new Plyr('#player');

// Remove all dblclick stuffs
player.eventListeners.forEach(function (eventListener) {
    if (eventListener.type === 'dblclick') {
        eventListener.element.removeEventListener(eventListener.type, eventListener.callback, eventListener.options);
    }
});

// Create overlay that will show the skipped time
const skip_ol = createElement("div");
skip_ol.id = "plyr__time_skip"
byClass("plyr")[0].appendChild(skip_ol)

// A class to manage multi click count and remember last clicked side (may cause issue otherwise)

class multiclick_counter {
    constructor() {
        this.timers = []; // collection of timers. Important 
        this.count = 0; // click count
        this.reseted = 0; // before resetting what was the count
        this.last_side = null; // L C R 3sides
    }

    clicked() {
        this.count += 1
        var xcount = this.count; // will be checked if click count increased in the time
        this.timers.push(setTimeout(this.reset.bind(this, xcount), 500)); // wait till 500ms for next click

        return this.count
    }

    reset_count(n) {
        // Reset count if clicked on the different side
        this.reseted = this.count
        this.count = n
        for (var i = 0; i < this.timers.length; i++) {
            clearTimeout(this.timers[i]);
        }
        this.timer = []

    }

    reset(xcount) {
        if (this.count > xcount) { return } // return if clicked after timer started
        // Reset otherwise
        this.count = 0;
        this.last_side = null;
        this.reseted = 0;
        skip_ol.style.opacity = "0";
        this.timer = []
    }

}

var counter = new multiclick_counter();

const poster = byClass("plyr__poster")[0]
// We will target the poster since this is the only thing sits between video and controls

poster.onclick = function (e) {
    const count = counter.clicked()
    if (count < 2) { return } // if not double click

    const rect = e.target.getBoundingClientRect();
    const x = e.clientX - rect.left; //x position within the element.
    const y = e.clientY - rect.top;  //y position within the element.
    console.log("Left? : " + x + " ; Top? : " + y + ".");
    // The relative position of click on video

    const width = e.target.offsetWidth;
    const perc = x * 100 / width;

    var panic = true; // panic if the side needs to be checked
    var last_click = counter.last_side

    if (last_click == null) {
        panic = false
    }

        if (perc < 40) {
      if(player.currentTime==0){
        return // won't seek beyond 0
      }
        counter.last_side = "L"
        if (panic && last_click != "L") {
            counter.reset_count(1)
            return
        }

        skip_ol.style.opacity = "0.9";
        player.rewind()
        skip_ol.innerText = "⫷⪡" + "\\n" + ((count - 1) * 10) + "s";

    }
    else if (perc > 60) {
    if(player.currentTime==player.duration){
    return // won't seek beyond duration 
  }
        counter.last_side = "R"
        if (panic && last_click != "R") {
            counter.reset_count(1)
            return
        }

        skip_ol.style.opacity = "0.9";
        last_click = "R"
        player.forward()
        skip_ol.innerText = "⪢⫸ " + "\n" + ((count - 1) * 10) + "s";

    }
    else {
        player.togglePlay()
        counter.last_click = "C"
    }

}

The CSS part:

#plyr__time_skip {
    background: #111111cc;
    border: 0;
    border-radius: 50%;
    color: #fff;
    left: 50%;
    min-width: 80px;
    width: min-content;
    max-width: 100px;
    max-height: 90px;
    opacity: 0;
    display: table-cell;
    text-align: center;
    vertical-align: middle;
    transform: translate(-50%, -50%);
    padding-top: 20px;
    position: absolute;
    top: 50%;
    transition: 1s;
    z-index: 3;
    pointer-events: none;
    box-shadow: 0px 0px 45px #000000;
}

Demo:

https://user-images.githubusercontent.com/34002411/192063572-eb1ed0b7-7fca-4868-a17c-5c3186ad10b9.mp4

trollwinner commented 1 year ago

I used @RaSan147 code for my needs with own implementation and multiple videos.

JS

for (let video of document.querySelectorAll('video')) {
    const player = new Plyr(video);

    player.on('ready', () => {
        const root = video.closest('.plyr-video');

        // remove double click handlers
        player.eventListeners.ForEach (function (EventListener) {
            if (eventListener.type === 'dblclick') {
                eventListener.element.removeEventListener(eventListener.type, eventListener.callback, eventListener.options);
            }
        });

        const poster = root.querySelector('.plyr__poster');
        const timeSkip = document.createElement('div');
        const resetState = () => {
            poster.clickedTimes = 0;
            poster.lastSideClicked = undefined;
        };

        timeSkip.className = 'plyr__time-skip';
        poster.parentNode.insertBefore(timeSkip, poster);
        poster.clickedTimes = 0;

        // handle clicks
        poster.addEventListener('click', function (event) {
            poster.clickedTimes++;

            if (poster.resetTimeout) {
                clearTimeout(poster.resetTimeout);
            }

            poster.resetTimeout = setTimeout(resetState, 1000);

            // handle only double click
            if (poster.clickedTimes < 2) {
                return;
            }

            // find click position
            const percentage = (event.clientX - event.target.getBoundingClientRect().left) * 100 / event.target.offsetWidth;

            if (percentage < 40) {
                if (player.currentTime === 0
                    || (typeof poster.lastSideClicked !== 'undefined' && poster.lastSideClicked !== 'L')
                ) {
                    clearTimeout(poster.resetTimeout);
                    resetState();

                    return;
                }

                timeSkip.innerText = '<<\n' + ((poster.clickedTimes - 1) * 10) + 's';
                timeSkip.classList.add('is-left');
                timeSkip.classList.remove('is-right');
                timeSkip.classList.remove('is-animated');
                setTimeout(() => timeSkip.classList.add('is-animated'), 1);
                poster.lastSideClicked = 'L';
                player.rewind();
            } else if (percentage > 60) {
                if (player.currentTime === player.duration
                    || (typeof poster.lastSideClicked !== 'undefined' && poster.lastSideClicked !== 'R')
                ) {
                    clearTimeout(poster.resetTimeout);
                    resetState();

                    return;
                }

                timeSkip.innerText = '>>\n' + ((poster.clickedTimes - 1) * 10) + 's';
                timeSkip.classList.add('is-right');
                timeSkip.classList.remove('is-left');
                timeSkip.classList.remove('is-animated');
                setTimeout(() => timeSkip.classList.add('is-animated'), 1);
                poster.lastSideClicked = 'R';
                player.forward();
            } else {
                poster.lastSideClicked = 'C';
            }
        });
    });
}

SCSS

@keyframes plyr__time-skip {
    40% {
        opacity: 1;
    }

    100% {
        opacity: 0;
    }
}

.plyr {
    &__time-skip {
        position: absolute;
        top: 0;
        bottom: 0;
        z-index: 10;
        display: flex;
        align-items: center;
        justify-content: center;
        color: #fff;
        width: 40%;
        opacity: 0;
        pointer-events: none;

        &.is-left {
            left: 0;
            background: linear-gradient(90deg, rgba(0, 0, 0, 0.5) 0%, transparent 100%);
        }

        &.is-right {
            right: 0;
            background: linear-gradient(90deg, transparent 0%, rgba(0, 0, 0, 0.5) 100%);
        }

        &.is-animated {
            animation: plyr__time-skip ease 1s forwards;
        }
    }
}