shiyiya/oplayer

Play Separated Video & Audio Files

namdevel opened this issue · 71 comments

hello, how to play separated video and audio files

example i have 2 file

  1. video without audio sound
  2. audio sound of video

@namdevel

You can try this, but why not use ffmpeg to merge them?

class SeparatedAudio implements PlayerPlugin {
  key = 'hello'
  name = 'oplayer-plugin-hello'
  version = __VERSION__

  private $audio: HTMLAudioElement = document.createElement('audio')

  constructor(private audioSrc: string) {
    this.$audio.src = this.audioSrc
  }

  apply(player: Player) {
    player.on(['play', 'pause'], ({ type }) => {
      //@ts-ignore
      this.$audio[type]()
      this.$audio.currentTime = player.currentTime
    })

    player.on(['seeking', 'error'], () => {
      this.$audio.pause()
    })

    const loading = $.cls('loading')
    player.on(() => {
      if (player.$root.classList.contains(loading)) {
        this.$audio.pause()
      } else {
        if (player.isPlaying && this.$audio.paused) {
          this.$audio.play()
        }
      }
    })

    player.on('seeked', () => {
      this.$audio.currentTime = player.currentTime
      if (player.isPlaying) this.$audio.play()
    })

    player.on('volumechange', () => {
      this.$audio.volume = player.volume
    })

    player.on('ratechange', () => {
      this.$audio.playbackRate = player.playbackRate
    })
    // .... other
  }

  destroy() {
    this.$audio.remove()
    URL.revokeObjectURL(this.$audio.src)
  }
}

@shiyiya because the audio and video hosted in separate files

@shiyiya and why i seeing this, player.context.ui.menu.notice is not a function

@shiyiya and why i seeing this, player.context.ui.menu.notice is not a function

use latest version @oplayer/core @oplayer/ui

yes, here is my code

<div id="oplayer"></div>
<script src="https://cdn.jsdelivr.net/npm/@oplayer/core@latest/dist/index.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@oplayer/ui@latest/dist/index.core.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@oplayer/core@latest/dist/index.ui.js"></script>

<script>
  const audio = new Audio("https://ngewibu.test/stream/audio/1/12771946.mp4");
  audio.load();
  const UIPlayer = OPlayer.ui({
    fullscreen: true,
    coverButton: true,
    miniProgressBar: true,
    autoFocus: true,
    forceLandscapeOnFullscreen: true,
    screenshot: true,
    pictureInPicture: false,
    showControls: "always",
    keyboard: { focused: true, global: false },
    settings: ["loop"],
    theme: { primaryColor: "#ffc107" },
    speeds: ["2.0", "1.75", "1.25", "1.0", "0.75", "0.5"],
    slideToSeek: "none",
    controlBar: false, // | { back:  'always' | 'fullscreen' } // appbar
    topSetting: false, //show setting icon on appbar
    /* Default value, Optional */

    /* Custom options */
    subtitle: {
      color: "white",
      fontSize: 30,
      fontFamily: "",
      source: [
        {
          name: "Bahasa Indonesia",
          default: true,
          src: "sub.vtt",
        },
      ],
    },
    thumbnails: {
      number: 100,
      src: "https://oplayer.vercel.app/thumbnails.jpg",
    },
  });

  const vplayer = OPlayer.make("#oplayer", {
    source: {
      src: "https://ngewibu.test/stream/videos/1/12771946.mp4",
      poster:
        "https://ngewibu.test/static/img/ogv/5d04814e48ef572e97cb18bbe928bbf8086c38cf.png@720w_405h_1e_1c_90q.webp",
      title: "Ngewibu.test",
    },
  }).use([UIPlayer]);
  vplayer.create();
  vplayer.context.ui.menu.notice("test hello"); // test notice
</script>
<script>
  function playMedia() {
    audio.play();
  }

  function pauseMedia() {
    audio.pause();
  }
  function seekMedia() {
    audio.currentTime = vplayer.currentTime;
  }
  // Keep audio and video in sync
  function syncMedia() {
    audio.currentTime = vplayer.currentTime;
  }

  // Event listeners
  vplayer.on("play", playMedia);
  vplayer.on("durationchange", syncMedia);
  vplayer.on("pause", pauseMedia);
  vplayer.on("seeked", seekMedia);
</script>
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="oplayer"></div>
    <script src="https://cdn.jsdelivr.net/npm/@oplayer/core@latest/dist/index.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/@oplayer/ui@latest/dist/index.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/@oplayer/hls@latest/dist/index.hls.js"></script>

    <script>
      class SeparatedAudio {
        constructor(audioSrc) {
          this.audioSrc = audioSrc
          this.key = 'hello'
          this.name = 'oplayer-plugin-hello'
          this.$audio = document.createElement('audio')
          this.$audio.src = this.audioSrc
        }
        apply(player) {
          player.on(['play', 'pause'], ({ type }) => {
            this.$audio[type]()
            this.$audio.currentTime = player.currentTime
          })
          player.on(['seeking', 'error'], () => {
            this.$audio.pause()
          })
          const loading = OPlayer.$.cls('loading')
          player.on(() => {
            if (player.$root.classList.contains(loading)) {
              this.$audio.pause()
            } else {
              if (player.isPlaying && this.$audio.paused) {
                this.$audio.play()
              }
            }
          })
          player.on('seeked', () => {
            this.$audio.currentTime = player.currentTime
            if (player.isPlaying) this.$audio.play()
          })
          player.on('volumechange', () => {
            this.$audio.volume = player.volume
          })
          player.on('ratechange', () => {
            this.$audio.playbackRate = player.playbackRate
          })
          // .... other
        }
        destroy() {
          this.$audio.remove()
          URL.revokeObjectURL(this.$audio.src)
        }
      }

      const UI = OUI({
        fullscreen: true,
        coverButton: true,
        miniProgressBar: true,
        autoFocus: true,
        forceLandscapeOnFullscreen: true,
        screenshot: true,
        pictureInPicture: false,
        showControls: 'always',
        keyboard: { focused: true, global: false },
        settings: ['loop'],
        theme: { primaryColor: '#ffc107' },
        speeds: ['2.0', '1.75', '1.25', '1.0', '0.75', '0.5'],
        slideToSeek: 'none',
        controlBar: false, // | { back:  'always' | 'fullscreen' } // appbar
        topSetting: false, //show setting icon on appbar
        /* Default value, Optional */

        /* Custom options */
        subtitle: {
          color: 'white',
          fontSize: 30,
          fontFamily: '',
          source: [
            {
              name: 'Bahasa Indonesia',
              default: true,
              src: 'sub.vtt',
            },
          ],
        },
        thumbnails: {
          number: 100,
          src: 'https://oplayer.vercel.app/thumbnails.jpg',
        },
      })

      const vplayer = OPlayer.make('#oplayer', {
        source: {
          src: 'https://ngewibu.test/stream/videos/1/12771946.mp4',
          poster: 'https://ngewibu.test/static/img/ogv/5d04814e48ef572e97cb18bbe928bbf8086c38cf.png@720w_405h_1e_1c_90q.webp',
          title: 'Ngewibu.test',
        },
      })
        .use([UI, new SeparatedAudio('https://ngewibu.test/stream/audio/1/12771946.mp4')])
        .create()

      vplayer.context.ui.notice('test hello') // test notice
    </script>
  </body>
</html>

thank you @shiyiya
can this player add watermark image ?
like in top left or whenever custom position ?

thank you @shiyiya can this player add watermark image ? like in top left or whenever custom position ?

document.createlm(img)
img.src =''
img.style.position= ''
vplayer.$root.appendChild(img)


thank you

var img = document.createElement("img");
img.src = "layer.png";
img.style.position = "absolute";
img.style.top = "10px";
img.style.right = "10px";
img.style.width = "200px";
img.style.height = "auto";
vplayer.$root.appendChild(img);

i hope you can add watermark in Oplayer options in future
I wanted to take a moment to express my sincerest thanks for your incredible work.

i hope you can add watermark in Oplayer options in future I wanted to take a moment to express my sincerest thanks for your incredible work.

I will consider it, thank u 😄

Screenshot_76

i'm seeing this error while take a screenshot

Screenshot_76

i'm seeing this error while take a screenshot

videoAttr: { crossorigin: 'anonymous' }, // screenshot

how to add custom slide in settings menu ?
like loop menu but custom and handle on off

how to add custom slide in settings menu ? like loop menu but custom and handle on off

setting.register(<Setting>{

i'm confused ^_^

player.context.ui.setting.register()

tester.html:117 Uncaught TypeError: Cannot read properties of undefined (reading 'setting')

oh sorry my mistakes

Screenshot_77

become default controls in mobile views/device

The real machine is not the same as the fake

截图_77

i try in my real iOS device and it's same , that was appeared while in the fullscreen mode

i try in my real iOS device and it's same , that was appeared while in the fullscreen mode

Yes, this is normal, ios can only video in full screen

AAh ok, thank you

WhatsApp Image 2023-05-20 at 20 17 34

while i try use another video player, they can show custom controls in my Device

WhatsApp Image 2023-05-20 at 20 17 34

while i try use another video player, they can show custom controls in my Device

try npm i oplayer/core@1.2.26-beta.1

is that available on jsdeliver version ?
because i'm not use npm

<script src="https://cdn.jsdelivr.net/npm/@oplayer/core@latest/dist/index.min.js"></script>

replace
<script src="https://cdn.jsdelivr.net/npm/@oplayer/core@1.2.26-beta.1/dist/index.min.js"></script>

how to add multiple thumbnails

image 1
image 2

x : 10
y: 10

x_sizes: 160
y_sizes: 90

how to add multiple thumbnails

image 1 image 2

x : 10 y: 10

x_sizes: 160 y_sizes: 90

https://oplayer.vercel.app/ui/#thumbnails
default size is 160 90,

thumbnails = {
  src: ['image1','image2'],
  x: 10,
  y: 6,
  number: 60
}

not work, the thumbnails not same with video time

not work, the thumbnails not same with video time

Can you provide an example?

403
If you can provide accessible videos and pictures, I can try to fix the error tomorrow

forbidden access from videoplayer or from url ?

yea

forbidden access from videoplayer or from url ?

not work, the thumbnails not same with video time

Of course, it is possible that the number of thumbnails generated is small and the gaps are too large so they are not that accurate

thumb
thumb-1

video duration is 24:00 minutes

video duration is 24:00 minutes

thumbnails = {
src: ['image1','image2'],
x: 10,
y: 10,
number: 192
}

Can you provide a complete example, fork template, add video address and thumbnails configuration
https://codesandbox.io/s/oplayer-m4inym

Screenshot_78

i'ts normal ?

I'm not sure if it's normal, it depends on the number of thumbnails you generate, for example, if the video is 10 minutes long and 2 thumbnails are generated, then it's quite inaccurate.
If your video is 24 minutes long and there are only 200 thumbnails, then the error of one thumbnail is 24*60/200=13.xxs. As long as the image exists before or after 13s of the video, it means it is normal.

Screenshot_79
but on bilibili that was same, and in bilibili website player it seem like your player

OK, it is certain that it should be an issue with oplayer.
But to fix it you need to provide videos and thumbnails 😂

Can you provide a complete example, fork template, add video address and thumbnails configuration https://codesandbox.io/s/oplayer-m4inym

i has been add

hmm how to save code that i has been modified ?

hmm how to save code that i has been modified ?

just ctrl+s and then share the link to me in the top right corner, or just copy the link in the address bar?

https://4r7q6f.csb.app/

Not this one, but a link like codesanbox.io/xxxx

OK, this is also possible

you can click open sandbox in bottom right

https://codesandbox.io/s/4r7q6f

it's like the thumbnail is 10s faster than video

i mean the 3:53 thumnails is appeared on 3:43

i got error, Maybe I don't have access?

MEDIA_ERR_SRC_NOT_SUPPORTED

it's seem your country blocked by bilibili server
you from china why bilibili blocked your country

bilibili also from china

Featured 🤡

I'll fixing it tomorrow!
I am here late at night

ok, thank you

@namdevel I guess you can set the number to 200 and try

Screenshot_80
if i set 200, the image red arrow in thumbnails become green arrow

red arrow is 03:53 minutes

this is the json about thumbnail on bilibili

{
  "code": 0,
  "message": "0",
  "ttl": 1,
  "data": {
    "pv_data": "https://pic.bstarstatic.com/videoshot/n230403er32xsq8sww4izs2q666shb65.bin",
    "x_len": 10,
    "y_len": 10,
    "x_size": 160,
    "y_size": 90,
    "images": [
      "https://pic.bstarstatic.com/videoshot/n230403er32xsq8sww4izs2q666shb65.jpg",
      "https://pic.bstarstatic.com/videoshot/n230403er32xsq8sww4izs2q666shb65-1.jpg"
    ]
  }
}

You can try pulling the bilibili video to the end to see if the video and thumbnails match。

the thumbnails match

number182, 3:53match but ed maybe not match

No idea

not work, the thumbnails not same with video time

Hey, you can play both at the same time, but it won't sync perfectly between audio and video. There will be slightly out of sync.

There is 1 solution for this (without doing merging both in the server), it is using mpeg dash. But you would need info such as bandwidth, etc....

Here is an example of how a mpeg dash file look like:

<?xml version="1.0"?>
<MPD xmlns="urn:mpeg:dash:schema:mpd:2011" profiles="urn:mpeg:dash:profile:isoff-on-demand:2011" minBufferTime="PT1M" type="static" mediaPresentationDuration="PT0H49M30S">
    <Period duration="PT0H49M30S">
        <AdaptationSet id="1" group="1" par="16:9" segmentAlignment="true" subsegmentAlignment="true" subsegmentStartsWithSAP="1" maxWidth="3840" maxHeight="2160" maxFrameRate="16000/656" startWithSAP="1">
            <Representation id="0" mimeType="video/mp4" codecs="avc1.640033" width="3840" height="2160" frameRate="16000/656" sar="1:1" bandwidth="7890794">
                <BaseURL>Video URL here</BaseURL>
                <SegmentBase indexRangeExact="true" indexRange="995-8154">
                    <Initialization range="0-994"/>
                </SegmentBase>
            </Representation>
        </AdaptationSet>
        
        <AdaptationSet id="2" group="2" subsegmentAlignment="true" subsegmentStartsWithSAP="1" segmentAlignment="true" startWithSAP="1">
            <Representation id="8" mimeType="audio/mp4" codecs="mp4a.40.2" bandwidth="180677">
                <BaseURL>Audio URL</BaseURL>
                <SegmentBase indexRangeExact="true" indexRange="934-8105">
                    <Initialization range="0-933"/>
                </SegmentBase>
            </Representation>
        </AdaptationSet>
    </Period>
</MPD>

If you managed to create a file like that, just use a player that support mpeg dash then audio and video should be sync'ed

how to get codecs type ?

how to get codecs type ?

ffmpeg

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta name="referrer" content="no-referrer" />
    <title>Start streaming now using OPlayer - Free HTML5 Player</title>
    <style>
      p {
        margin: 0;
      }
      h4 {
        margin: 10px 0 4px;
      }
      .close {
        position: absolute;
        right: 10px;
        top: 10px;
        cursor: pointer;
        font-size: 1em;
      }
    </style>
  </head>
  <body style="margin: 0; background-color: #000">
    <div id="oplayer" style="width: 100vw; height: 100vh"></div>

    <script src="https://cdn.jsdelivr.net/npm/@oplayer/core@latest/dist/index.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/@oplayer/ui@latest/dist/index.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/@oplayer/hls@latest/dist/index.hls.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/mp4box@0.5.2/dist/mp4box.all.min.js"></script>

    <script>
      const query = document.location.search.substring(1)
      window.player = OPlayer.make('#oplayer', {
        source: {
          src: 'https://ohplayer.netlify.app/%E5%90%9B%E3%81%AE%E5%90%8D%E3%81%AF.mp4',
        },
        playbackRate: localStorage.getItem('@oplayer/UserPreferences/speed') || 1,
        volume: localStorage.getItem('@oplayer/UserPreferences/volume') || 1,
      })
        .use([
          OUI({ keyboard: { global: true } }),
          OHls(() => true),
          {
            apply(player) {
              var mp4box = MP4Box.createFile()
              mp4box.onReady = showVideoInfo

              fetch(player.options.source.src, {
                headers: {
                  range: 'bytes=0-50000',
                },
              })
                .then((it) => it.arrayBuffer())
                .then((arrayBuffer) => {
                  arrayBuffer.fileStart = 0
                  mp4box.appendBuffer(arrayBuffer)
                })

              let box
              player.context.ui.keyboard.register({
                v: () => {
                  box.style.display = box.style.display == 'none' ? 'block' : 'none'
                },
              })
              function showVideoInfo(info) {
                console.log(info)
                const videoTrack = info.tracks[0]
                const audioTrack = info.tracks[1]
                box = document.createElement('div')
                box.innerHTML = `
                  <p>${'Duration: ' + parseInt(info.duration / 1000) + 's'}</p>
                  <p>${'Brands: ' + info.brands.join(',')}</p>
                  <h4>Video Metadata</h4>
                  <p>${'Video Codec: "' + videoTrack.codec + '"; nb_samples: ' + videoTrack.nb_samples}</p>
                  <p>${videoTrack.name + ': size: ' + videoTrack.size + '; bitrate: ' + videoTrack.bitrate}</p>
                  <h4>Audio Metadata</h4>
                  <p>${'Audio Codec: "' + audioTrack.codec + '"; nb_samples: ' + audioTrack.nb_samples}</p>
                  <p>${audioTrack.name + ': size: ' + audioTrack.size + '; bitrate: ' + audioTrack.bitrate}</p>
                  <div class="close">[x]</div>
                  `

                box.style.cssText = `
                  position: absolute;
                  left: 0;
                  top: 0;
                  padding: 20px;
                  background-color: rgba(0,0,0, .5);
                  color: #54ab3d;
                  color: var(--primary-color);
                  font-size: 0.875em;
                  border-radius: 4px;`
                player.context.ui.$root.appendChild(box)
              }
            },
          },
        ])
        .create()
        .on(console.log)
    </script>
  </body>
</html>

ok thank you, any update about thumbnails ??

ok thank you, any update about thumbnails ??

I don't think oplayer is wrong.

not work, the thumbnails not same with video time

fixed! @oplayer/ui 1.2.28

@namdevel If you still have any questions, you can start a new issue.