在上一篇的發文中,對於小專案youtube點播機
己完成了播放機的本體與清單
接下來要處理與播放有關的部份
隨機播放,順序播放,及上傳播放清單的功能
首先要處理的就是要取得播放影片的狀態
像是播放完成的事件
以及自動播放的功能
為了可以取得播放器的播放狀態,之前用<iframe>
的方式只能播放,
無法得知是否播放完成了
因此,需要引入Youtube 的 IFrame Player API
官網的說明連結如下
YouTube 開發人員說明文件
https://developers.google.com/youtube/documentation?hl=zh-tw
IFrame Player API 參考資料
https://developers.google.com/youtube/iframe_api_reference?hl=zh-tw
YouTube 播放器參數
https://developers.google.com/youtube/player_parameters?hl=zh-tw
首先要透過IFrame Player API來建立一個 YT.Player 的物件
此物件會產生 <ifame>
的播放元件,這樣就可以完全控制 YT.Player 的操作,
也可以讀取播放器的狀態了
建立一個 YT.Player 的物件的程式碼如下
<!DOCTYPE html>
<html>
<body>
<div id="ytplayer"></div>
<script>
var tag = document.createElement('script');
tag.src = "https://www.youtube.com/iframe_api";
var firstScriptTag = document.getElementsByTagName('script')[0];
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
let player;
function onYouTubeIframeAPIReady() {
player = new YT.Player('ytplayer', {
height: '390',
width: '640',
videoId: 'M7lc1UVf-VE',
playerVars: {
'playsinline': 1
},
events: {
'onReady': onPlayerReady,
'onStateChange': onPlayerStateChange
}
});
}
function onPlayerReady(event) {
event.target.playVideo();
}
let done = false;
let pid = 0;
function onPlayerStateChange(event) {
console.log(event.data);
if (event.data == 0 ) {
done = true;
}
if(done){
done = false;
$("#app")[0]._vnode.component.ctx.change_yt();
}
}
function stopVideo() {
player.stopVideo();
}
</script>
</body>
</html>
步驟1: 建立預備給player用的 <div id="ytplayer"></div>
步驟2: 匯入 IFrame Player API
var tag = document.createElement('script');
// 產生<script>
tag.src = "https://www.youtube.com/iframe_api";
// 設定 script的src 是 https://www.youtube.com/iframe_api 就是api的來源
var firstScriptTag = document.getElementsByTagName('script')[0];
// 找出第一個<script>的元素
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
// 將新產生的<script>加在第一個<script>的元素的前面,至於為什麼要放在第一個<script>處,就先這樣了解就好
步驟3: 建立player元件
let player;
// 宣告player變數
function onYouTubeIframeAPIReady() { //當api載入完成時,執行以下程式
player = new YT.Player('ytplayer', { //建立 YT.Player() 元件,其中 'ytplayer' 對應到 <div id="ytplayer"></div> 的id
height: '390', // 設定播放器的 width,height,videoId 是預先要載入的影片的videoID
width: '640',
videoId: 'M7lc1UVf-VE',
playerVars: {
'playsinline': 1 //如下說明
},
events: {
'onReady': onPlayerReady, //設定2個常用的事件 onReady 播放器準備好時,
'onStateChange': onPlayerStateChange // 播放器狀態改變時
}
});
}
playsinline 這個參數,參考官網的說明如下,看起來是針對 iOS 裝置的設定,
0 是指在iOS 裝置上以全螢幕模式播放
1 是指在行動瀏覽器以內嵌方式播放
不過對桌機瀏覽器來說,這個參數也可以先忽略
再來是 播放器的狀態 對照表
-1 – 尚未開始
0 – 已結束
1 – 播放中
2 – 已暫停
3 – 緩衝處理中
5 – 提示的影片
所以要確認影片是否播完,可以檢查狀態是否為 0
就可以執行切換下一首的操作
步驟4: 設定事件要執行的動作
function onPlayerReady(event) { // 當播放器準備好就開始播放影片
event.target.playVideo();
}
let done = false;
let pid = 0;
function onPlayerStateChange(event) { // 當播放器狀態改變時就執行
console.log(event.data); // 列出目前的狀態
if (event.data == 0 ) { // 若狀態代碼是0, 代表影片播完了
done = true; // 設定變數done 為true
}
if(done){ // 若done為true,執行切換影片的操作
done = false;
$("#app")[0]._vnode.component.ctx.change_yt();
}
}
function stopVideo() { // 設定影片停止的函式
player.stopVideo(); // 執行停止播放
}
其中 $("#app")[0]._vnode.component.ctx.change_yt();
的部份
是從 $("#app")
的查詢,找到詢此路徑可以呼叫到 在vue元件中設定的 method
也就是 change_yt()
的功能,會進行影片切換
切換的方式有隨機播放,也有輪播的方式
有時候元件有哪些功能可以使用,需要透過Chrome 的 DevTools來查看
console.log($("#app"))
會列出元件完整的 屬性,方法及樣式
這部份的程式是要寫在index.html本身的檔案上
不過為了版面的排列,將 <div id="ytplayer"></div>
移到 App.vue 的<template>
裡面
另外,因為會用到 p5.js 及 jQuery,w3.css
還要加入
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.2/p5.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<link rel="stylesheet" href="https://www.w3schools.com/w3css/4/w3.css">
//------------------------------
接下是就是
App.vue的部份
原本的
<div>
<iframe width="640" height="360" :src="ytlink" title="" frameborder="1" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
</div>
改成
<div>
<div id="ytplayer"></div>
</div>
在 player = new YT.Player('ytplayer' {})
建立後
就會代換為
<iframe id="ytplayer" frameborder="0" allowfullscreen="1" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" title="2 Hours Of Canon in D by Pachelbel (Most Popular Version) | Relaxing Music | Piano & Cello" width="640" height="360" src="https://www.youtube.com/embed/bHQqvYy5KYo?enablejsapi=1&origin=http%3A%2F%2Flocalhost%3A5173&widgetid=1"></iframe>
這時 <iframe>
的id變成 ytplayer
按鈕的配置作了一些調整
<button class="w3-btn w3-border w3-round-large" @click="add_yt">新增</button>
<button class="w3-btn w3-border w3-round-large" @click="record_yt">修改</button>
<button class="w3-btn w3-border w3-round-large" @click="delete_yt">刪除</button>
<button class="w3-btn w3-border w3-round-large" @click="change_yt">下一首</button>
<button class="w3-btn w3-border w3-round-large" @click="save_yt">下載清單</button>
@click="add_yt" -> 新增
@click="record_yt" -> 修改
@click="delete_yt" -> 刪除
@click="change_yt" -> 下一首
@click="save_yt" -> 下載清單
接下來針對有變動的部份進行說明
add_yt()
的部份
原本 this.ytlink = "https://www.youtube.com/embed/"+this.videoID+"?autoplay=1"
來綁定iframe 的src
改成 player.loadVideoById(this.videoID, 0);
就是載入videoID 來播放影片
setTimeout(()=>{
this.vt_title = $("#ytplayer").attr("title");
let yt_index = this.ytlist.findIndex((e) => e.videoID === this.videoID )
if(yt_index == -1 && this.videoID.length==11){
this.ytlist.push({videoID: this.videoID, videoTitle: this.vt_title, albumName: this.vt_album})
}
console.log($("#app")[0]._vnode.component.ctx.ytlist);
}, 2000);
因為 player.loadVideoById()
載入到iframe需要等待一下,就可以讀到 video的title標題了
因此用 setTimeout , 2秒後 讀到 video的title標題,然後加入到 播放清單 ytlist
edit_yt(n){
console.log(n)
this.videoID = n.videoID
this.vt_title = n.videoTitle
this.vt_album = n.albumName
this.ytlink = "https://www.youtube.com/embed/"+this.videoID+"?autoplay=1"
player.loadVideoById(this.videoID, 0);
}
同樣改成 player.loadVideoById(this.videoID, 0);
第1個參數 是 videoID,第2個參數時播放的開始秒數
change_yt(){
let v_cnt = this.ytlist.length;
// 先取得目前播放清單 ytlist 的數量
let vid = parseInt(Math.random()*v_cnt+"");
// 隨機取得一個編號 Math.random() 是 0~ 1, v_cnt是數量,Math.random()*v_cnt 是 0~ v_cnt
// Math.random()*v_cnt+"" 轉成字串, parseInt() 是取整數,也就是當作 索引值
this.edit_yt(this.ytlist[vid])
// this.ytlist[vid] 取的該索引值的播放資料
// 例如 { videoID: "Ptk_1Dc2iPY", videoTitle: "Canon in D - Cello & Piano", albumName: "Canon"},
// 然後 執行 this.edit_yt() 執行影片播放
}
以上的方法是 隨機播放的方式
若是要播輪的話
可以改成
let vid = 0;
change_yt(){
let v_cnt = this.ytlist.length;
let vid = (vid+1)%v_cnt;
// 取得下一首資料
this.edit_yt(this.ytlist[vid])
}
在下一首的計算上,有不用的規則
在新增時 會切換到 最新的影片
因此 this.vid = this.ytlist.length-1;
所以vid是影片數量減1
在刪除時 會停止目前的影片,切到下一首,
原則止vid不變,但影片數量少1, 所以 vid 要重新設定
變成
let v_cnt = this.ytlist.length;
this.vid = (this.vid)%v_cnt;
// 要特別這樣計算是因為這個狀況
// 若影片數量是 4, vid是3, 當3被刪除時 若 vid繼續是3, 會變成讀不到3
// 因為數量變成 3, 所以 3%3=0, vid要變成0, 才讀的到
// 也就是最後1筆刪除時,下一首就是跳回第1首了
this.edit_yt(this.ytlist[this.vid])
在修改時,會先點選其中一首
這時vid要改成該首的索引值
let yt_index = this.ytlist.findIndex((e) => e.videoID === this.videoID )
// 取的索引值
this.vid = yt_index;
最後是上傳播放清單的部份,
原則上是以下載的清單格式為準
也就是 JSON的格式,
下載後,可以根據JSON格式。進行清單的編輯,
然後再以將JSON清單內容檔案上傳上去,
同時更新成新的 播放清單
先新增上傳清單: <input type="file" class="w3-btn w3-border w3-round-large" id="jsnfile" name="jsnfile" @change="upload_yt" accept="*.json">
按下 「選擇檔案」 按鈕來挑選 .json 檔案,
當挑選完檔案後,就會觸發 @change 執行 upload_yt
accept=".json" 可以設定限制只能上傳 .json 檔案
接下來是 upload_yt()
async upload_yt(e){
const file = e.target.files.item(0)
this.jsn_data = await file.text();
console.log(JSON.parse(this.jsn_data))
this.ytlist = JSON.parse(this.jsn_data)
this.vid = 0
this.edit_yt(this.ytlist[this.vid])
}
最後還有一個需求
就是若只有videoID, 沒有 videoTitle 時
可以進行標題查詢,寫回 videoTitle 及 vt_title
edit_yt(n){ // n 代表點選到的影片資料
player.loadVideoById(this.videoID, 0); //切換影片後,等2秒
setTimeout(()=>{
this.vt_title = $("#ytplayer").attr("title"); // 取得影片的標題,並傳回 vt_title
n.videoTitle = this.vt_title // 同時也傳回 videoTitle
}, 2000);
}
補充一下 save_yt() 的部份
在設定下載清單的檔名時,為了不要只用同一個 youtube_list.json 的檔名
另外增加以當下時間為檔名的處理方式
save_yt(){
console.log(top)
//設定以'Asia/Taipei'為時區的時間, 格式是這樣 2023/10/13 02:34:10
let timex = (new Date()).toLocaleString( 'zh-TW', {
timeZone: 'Asia/Taipei',
hour12: false
})
timex = timex.replaceAll("/", "").replaceAll(":", "").replaceAll(" ", "_")
//將時間字串 改成這個格式 20231013_023410
console.log(timex);
// 最後的檔名變成 'youtube_list_'+timex 例如: youtube_list_20231013_023410
top.saveJSON(JSON.parse(JSON.stringify(this.ytlist)), 'youtube_list_'+timex)
//存檔後變成 youtube_list_20231013_023410.json
},
以上算是將 Vue版的 youtube點播機,使用到 IFrame Player API 來開發
這中間也有穿插使用 jQuery 及 p5.js, w3.css
或許有更好,更簡便的寫法
或是都可以用vue的方式來實作
不過總算是有用到vue的方式
結果是不錯的,Vue版的 youtube點播機 自己也用的很高興
還剩最後一篇了, 加油!!
以下是完整的程式碼
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.2/p5.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<link rel="stylesheet" href="https://www.w3schools.com/w3css/4/w3.css">
<title>Vue Youtube Player</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
<script>
var tag = document.createElement('script');
tag.src = "https://www.youtube.com/player_api";
var firstScriptTag = document.getElementsByTagName('script')[0];
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
let player;
function onYouTubePlayerAPIReady() {
player = new YT.Player('ytplayer', {
height: '360',
width: '640',
videoId: 'bHQqvYy5KYo',
events: {
'onReady': onPlayerReady,
'onStateChange': onPlayerStateChange
}
})
}
function onPlayerReady(event) {
event.target.playVideo();
}
let done = false;
let pid = 0;
function onPlayerStateChange(event) {
console.log(event.data);
if (event.data == 0 ) {
done = true;
}
if(done){
done = false;
$("#app")[0]._vnode.component.ctx.change_yt();
}
}
function stopVideo() {
player.stopVideo();
}
</script>
<script>
function setup(){
noCanvas();
noLoop();
}
</script>
</body>
</html>
main.js
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
app.mount('#app')
App.vue
<template>
<div>
<h3>Vue YouTube Player</h3>
<h4>videoID: {{ videoID }} ---- {{ vt_title }}</h4>
Input Youtube Album: <input class="w3-input w3-border w3-round" type="text" v-model="vt_album" @click="vt_album=''">
Input Youtube Title: <input class="w3-input w3-border w3-round" type="text" v-model="vt_title" @click="vt_title=''">
Input Youtube URL: <input class="w3-input w3-border w3-round" type="text" v-model="vt_url" @click="vt_url=''">
<button class="w3-btn w3-border w3-round-large" @click="add_yt">新增</button>
<button class="w3-btn w3-border w3-round-large" @click="record_yt">修改</button>
<button class="w3-btn w3-border w3-round-large" @click="delete_yt">刪除</button>
<button class="w3-btn w3-border w3-round-large" @click="change_yt">下一首</button>
<button class="w3-btn w3-border w3-round-large" @click="save_yt">下載清單</button>
上傳清單: <input type="file" class="w3-btn w3-border w3-round-large" id="jsnfile" name="jsnfile" @change="upload_yt" accept="*.json">
</div>
<!-- <div>
<iframe width="640" height="360" :src="ytlink" title="" frameborder="1" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
</div> -->
<div>
<div id="ytplayer"></div>
</div>
<table class="w3-table-all">
<tr><th>videoID</th><th>Video_Title</th><th>Album_Title</th></tr>
<tr v-for="x in ytlist">
<td @click="edit_yt(x)" style="cursor:pointer">{{ x.videoID }}</td><td>{{ x.videoTitle }}</td><td>{{ x.albumName }}</td>
</tr>
</table>
</template>
<script>
export default {
data() {
return {
vt_url: "https://www.youtube.com/watch?v=FqCw7gCTZQs&pp=ygUP5LmZ5aWz44Gp44KC44KI",
vt_album: "Vue YouTube Player",
vt_title: "",
videoID: "",
ytlink: "",
vid: 0,
jsn_data: "",
ytlist: [
{ videoID: "Ptk_1Dc2iPY", videoTitle: "Canon in D - Cello & Piano", albumName: "Canon"},
{ videoID: "7051Y4WVFJA", videoTitle: "2 Hours Of Canon in D by Pachelbel", albumName: "Canon"}
]
};
},
methods: {
add_yt() {
let str = this.vt_url;
let urlParams = new URLSearchParams(str.split("?")[1])
let vid = urlParams.get("v");
if(vid!=null){
this.videoID = vid;
} else {
let f1 = str.indexOf("?");
if(f1!=-1){
str = str.split("?")[0];
}
let f2 = str.indexOf("youtu.be");
let f3 = str.indexOf("embed");
let f4 = str.indexOf("shorts");
if(f2!=-1){
this.videoID = str.split("youtu.be/")[1];
} else if(f3!=-1){
this.videoID = str.split("embed/")[1];
} else if(f4!=-1){
this.videoID = str.split("shorts/")[1];
} else if(str.length==11){
this.videoID = str;
} else {
this.videoID = "";
}
}
this.ytlink = "https://www.youtube.com/embed/"+this.videoID+"?autoplay=1"
player.loadVideoById(this.videoID, 0);
setTimeout(()=>{
this.vt_title = $("#ytplayer").attr("title");
let yt_index = this.ytlist.findIndex((e) => e.videoID === this.videoID )
if(yt_index == -1 && this.videoID.length==11){
this.ytlist.push({videoID: this.videoID, videoTitle: this.vt_title, albumName: this.vt_album})
this.vid = this.ytlist.length-1;
}
console.log($("#app")[0]._vnode.component.ctx.ytlist);
}, 2000);
},
record_yt() {
let yt1 = this.ytlist.find((e) => e.videoID === this.videoID )
yt1.albumName = this.vt_album;
yt1.videoTitle = this.vt_title;
},
/**
* @param {{ videoID: string; videoTitle: string; albumName: string; }} n
*/
edit_yt(n){
console.log(n)
this.videoID = n.videoID
this.vt_title = n.videoTitle
this.vt_album = n.albumName
this.ytlink = "https://www.youtube.com/embed/"+this.videoID+"?autoplay=1"
let yt_index = this.ytlist.findIndex((e) => e.videoID === this.videoID )
this.vid = yt_index;
player.loadVideoById(this.videoID, 0);
setTimeout(()=>{
this.vt_title = $("#ytplayer").attr("title");
n.videoTitle = this.vt_title
}, 2000);
},
save_yt(){
console.log(top)
let timex = (new Date()).toLocaleString( 'zh-TW', {
timeZone: 'Asia/Taipei',
hour12: false
})
timex = timex.replaceAll("/", "").replaceAll(":", "").replaceAll(" ", "_")
console.log(timex);
top.saveJSON(JSON.parse(JSON.stringify(this.ytlist)), 'youtube_list_'+timex)
},
delete_yt(){
let yt_index = this.ytlist.findIndex((e) => e.videoID === this.videoID )
if(yt_index != -1){
this.ytlist.splice(yt_index, 1);
let v_cnt = this.ytlist.length;
this.vid = (this.vid)%v_cnt;
this.edit_yt(this.ytlist[this.vid])
}
},
change_yt(){
let v_cnt = this.ytlist.length;
// let vid = parseInt(Math.random()*v_cnt+"");
this.vid = (this.vid+1)%v_cnt;
this.edit_yt(this.ytlist[this.vid])
},
async upload_yt(e){
const file = e.target.files.item(0)
this.jsn_data = await file.text();
console.log(JSON.parse(this.jsn_data))
this.ytlist = JSON.parse(this.jsn_data)
this.vid = 0
this.edit_yt(this.ytlist[this.vid])
}
}
}
</script>
<style>
div {
padding:10px
}
button {
margin: 5px;
}
</style>