1. 程式人生 > >模擬網易雲的H5音樂播放器

模擬網易雲的H5音樂播放器

轉發我在github釋出的一個H5音樂播放器

前言

  • 這是我第一個GitHub專案,之前一直想在GitHub寫點東西,近期又在學前端,剛好學到audio標籤,平常時也比較喜歡聽音樂寫程式碼,因此就萌生了自己寫一個音樂播放器的想法。利用了下班和週末的空閒時間,用了兩週時間終於寫出來了。當用程式碼一個個地把自己的想法實現出來,那種興奮和成就感是無與倫比的。甚至週末的時候一碼就碼到通宵,還不覺得累哈哈。因為我也是初學,用GitHub把自己的註釋和思路寫出來,可以提供給跟我一樣的夥伴來練手。
  • 首先先要感謝兩位大神的知識分享:
    • CeuiLiSA:GitHub首頁 該音樂播放器是基於王樂平的基礎上修改的,借鑑了他的思路進行完善和新增功能。該部落格的連結為
      一步一步實戰HTML音樂播放器
      。實現的思路寫得非常清晰,讓我對寫播放器摸著了門路。而CeuiLiSA則詳細地分享了網易雲最新的介面網易雲音樂API,可以利用這些介面獲得歌單、歌曲、歌詞、專輯圖片等,讓播放器的功能更加完善。

音樂播放器效果

  • 我把音樂播放器放到了騰訊雲伺服器了,可以直接點選下面連結檢視效果,音樂播放器具備的功能基本都實現了。如當前歌單音樂列表、輸入網易雲使用者名稱獲取歌單進行切換、音樂列表歌名搜尋、網路音樂搜尋、電腦本地音樂新增播放、歌詞滾動顯示、歌曲播放進度等。

  • 音樂播放器的整個介面效果: 音樂播放器介面效果

  • 從上到下的佈局分為“音樂列表”“您的歌單”button按鈕、專輯圖片、歌名、歌手、歌詞、播放控制按鈕、歌曲時長、當前時間、播放進度條。

頁面程式碼佈局

<div class="player">
		<!--播放器頂部,用來裝載各種選單button-->
		<div class="header">
			<button id="musicBtn" class="button" onclick="showList()">音樂列表</button>
			<button id="back" class="button" onclick="goBack()" style="display: none;">返回</button>
			<button id="local" class="localButton" onclick="getLocalFiles()" style="display: none;">本地音樂</button>
			<button id="switch" class="localButton" onclick="myPrompt('綠水青山jv')">您的歌單</button>
			<button id="search" class="searchButton" onclick="promptSearch()" style="display: none;">搜尋</button>
			<span id="title" style="display: block;" onmouseover="showShortcut(this,event)" onmouseout="showShortcutOut()">音樂播放器</span>
		</div>
		<div>
			<!--音樂專輯圖片-->
			<div class="albumPic"></div>
			<!--音樂列表-->
			<div id="musicList" class="musicList">
				<iframe style="width: 100%;" src="music.html"></iframe>
			</div>
			<!--歌單列表-->
			<div id="songList" class="musicList">
				<iframe style="width: 100%;" src="songList.html"></iframe>
			</div>
			<!--本地音樂搜尋列表-->
			<div id="searchMusicList" class="musicList">
				<iframe style="width: 100%;" src="searchMusicList.html"></iframe>
			</div>
			<!--網路音樂搜尋列表-->
			<div id="netSearchList" class="musicList">
				<iframe style="width: 100%;" src="netSearchList.html"></iframe>
			</div>
			<!--本地資料夾音樂列表-->
			<div id="localMusicList" class="musicList">
				<iframe style="width: 100%;" src="localMusicList.html"></iframe>
			</div>
			<!--歌詞背景-->
			<div id="lyricDiv" class="musicList" 
				style="width:320px;
				background-image: linear-gradient( rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.2) ),url(img/clouds.jpeg);
				background-repeat: no-repeat; border: none;padding: 0px;
				text-align: center;
    			font-size: 20px;
    			color: #cbc7c7;
    			overflow: hidden;
    			position: relative;">
				<ul>
					<li>歌詞</li>
				</ul>
			</div>
		</div>
		<!--音樂資訊,歌名、歌手-->
		<div class="trackInfo">
			<div class="name"><p></p></div>
			<div class="artist"></div>
			<div class="album"></div>
		</div>
		<!--歌詞-->
		<div class="lyric">
			<p id="lyric" onclick="showFullLyric()" 
				onmouseover="lyricTip(this,event)" 
				onmouseout="tipOut()">
				歌詞
			</p>
		</div>
		<!--播放控制按鈕,上一首下一首-->
		<div class="controls">
			<div class="play">
				<i class="icon-play"></i>
			</div>
			<div class="previous">
				<i class="icon-previous"></i>
			</div>
			<div class="next">
				<i class="icon-next"></i>
			</div>
		</div>
		<!--顯示歌曲時間資訊,當前播放時間、歌曲總時長-->
		<div class="time">
			<div class="current"></div>
			<div class="total"></div>
		</div>
		<!--使用漸變顏色顯示歌曲時間進度條-->
		<div class="progress"></div>
		<!--載入動畫,正在使用網路載入音樂檔案時則顯示出來,完成則隱藏-->
		<div class="loader"></div>
		<div class="loader2"></div>
		<!--audio標籤裝載當前播放的音樂檔案-->
		<audio id="audio" preload="auto"><source src=""></audio>
	</div>
	<!--裝載使用者之前輸入的網易雲使用者名稱,幫助使用者快捷輸入-->
	<datalist id="nameLists"></datalist>
	<!--裝載使用者之前搜尋輸入的歌曲名,幫助使用者快捷輸入-->
	<datalist id="songLists"></datalist>
  • 除了主頁面顯示的元素,還隱藏著下面的元素: 隱藏元素
  • 這些元素會在按鈕點選的情況觸發顯示,元素的隱藏和顯示主要利用display的block或者none來控制。音樂列表的內容主要用iframe標籤內嵌頁面來顯示。

播放器實現的邏輯思路

  • 首先音樂播放器有三個重要的物件: 1、當前播放音樂:currentPlaySong 2、當前播放音樂列表:musicInfos 3、當前播放狀態:playStatus
  • currentPlaySong的屬性為:
	{	
		songListName: "";  //所屬的歌單名稱 
		songID:"";  //歌曲ID,網易雲音樂每首歌都有固定的ID,通過ID可以獲得歌曲的播放連結
		songName: "";//歌名
		artist: "";//歌手
		albumPic: "";//專輯圖片url
		totalTime: "";//歌曲總時長
		mp3Url: "";//該url的格式是http://music.163.com/song/media/outer/url?id=3986241
        mp3Url2: "";//該url的格式是"https://m7.music.126.net/20181016104636/965ff036084dc30ec291460d1f4f85e3/ymusic/8827/447f/9ef4/3f399b1fd6d919555b55691e6632366d.mp3"
        上面兩個是不同的連結格式,第一個連結我公司的網路設定了禁止訪問網易雲音樂163網站,所以無法播放,但一般的網路都可以訪問。
        第二個連結則是通過網易雲api介面獲取到的,公司的網路沒有禁止,可以訪問,當然一般的網路也可以訪問。
        兩個連結都同時新增到audio下的source下,一個連結失效了,另一個連結可以使用。
		connectTimes: ""; //記錄從網易雲api介面獲取了多少次mp3Url連結,超過一定的次數則停止獲取,避免ID失效,死迴圈獲取
		index: "" //是在歌曲列表中的第幾首歌
	};
  • musicInfos就是一個數組,儲存列表中的所有音樂物件。
  • playStatus的屬性為:
{
	currentTrackLen: 0; //當前播放列表總的歌曲數 
	currentTrackIndex: 0;  //當前播放的是列表中的第幾首歌
	currentTime: 0;  //當前播放的時長
	currentTotalTime: 0;  //當前播放歌曲的總時長
	playTimes: 0;  //點選上一首下一首的總次數,當達到10次就更新網易雲介面獲取的mp3Url連結,因為這個連結網易雲設定了失效,30分鐘左右就會失效
	_playStatus: false;  //使用下劃線字首表示這是受保護的物件,即protected
};
  • 主要的流程為: 載入流程

非同步獲取歌單列表的程式碼為:

//獲取歌單的所有歌曲資訊
function initSongs(url) {
	var connectUrl = "";
	if(url) {
		connectUrl = url;
	} else {
		connectUrl = playlistUrl + playListID;
	}
	//通過ajax同步請求資料,如果網路異常則給出提示
	$.ajax({
		url: connectUrl,
		type: "post",
		dataType: 'JSON',
		async: false,
		cache: true,
		success: getSongId,
		error: function(xhr, status, error) {
			//alert("抱歉,伺服器故障了。\n你可以播放預設歌曲和本地音樂。");
			//如果網路問題或者伺服器掛了,則載入預先下載好的歌單資訊
			getSongId(playListTest);
		}
	});
}

//根據網易雲介面返回的資訊初始化歌曲列表
function getSongId(data) {
	var tracks = data.playlist.tracks;
	//重新整理歌曲之前,先把之前的歌曲置空
	musicInfos = [];
	for(var i = 0; i < tracks.length; i++) {
		var musicInfo = new Object();
		//只在第一個物件中儲存歌單名稱
		if(i == 0) {
			musicInfo.songListName = data.playlist.name;
		}
		musicInfo.songID = tracks[i].id;
		musicInfo.songName = tracks[i].name;
		//為了簡潔,只獲取第一個歌手的名
		musicInfo.artist = tracks[i].ar[0].name;
		musicInfo.albumPic = tracks[i].al.picUrl + '?param=270y270';
		musicInfo.totalTime = tracks[i].dt;
		musicInfo.mp3Url = "http://music.163.com/song/media/outer/url?id=" + tracks[i].id + ".mp3";
		musicInfo.mp3Url2 = ""; //第二個連結先置空,後面利用空閒時間非同步獲取,保證應用效能
		//connetTimes記錄當前歌曲從介面獲取mp3Url連線的次數,
		//超過5次則停止獲取,
		//避免歌曲ID失效,網易雲介面傳過來的是空url,造成浪費資源多次獲取
		musicInfo.connectTimes = 0;
		musicInfo.index = i;
		musicInfos.push(musicInfo);
	}

	//初始完成之後主要把賦值給正在播放的列表
	currentPlayList = musicInfos;
	currentPlayListIndex = 0;

	//現在有了新的解決mp3Url的方法,就是獲取到歌曲ID後直接
	//在該連結後面加上ID的值http://music.163.com/song/media/outer/url?id= + id.mp3,
	//這為獲取url連結提供了極大的方便,
	//既不用擔心url連結失效,也不用擔心獲取url的介面失效或者IP被禁,
	//也避免了多次耗費資源迴圈地更新url連結

	//先同步獲取第一首歌曲的第二個mp3Url2連結,
	//後面才非同步獲取當前播放歌曲的前10首和後10首。
	//由於網易雲介面獲取的url有時間限制,超過大概半個小時後url連結就會失效,
	//所以使用者點選上一首或下一首共10次之後或者半個小時之後就會更新連結
	getMp3Url(musicInfos, 0, 1, 0, false);
}

//var errorCount = 0;
//initList當前需要獲取url連結的陣列,為musicInfos或者songSearchResults
function getMp3Url(initList, startindex, endIndex, goalIndex, isAsync, isCached) {

	for(var i = startindex; i < endIndex; i++) {
		var songUrl = "";
		if(goalIndex) {
			songUrl = "https://api.imjad.cn/cloudmusic/?type=song&id=" + initList[goalIndex].songID + "&br=128000";
		} else {
			songUrl = "https://api.imjad.cn/cloudmusic/?type=song&id=" + initList[i].songID + "&br=128000";
		}
		$.ajax({
			url: songUrl,
			type: "get",
			dataType: 'JSON',
			crossDomain: true,
			async: isAsync,
			cache: isCached ? isCached : isAsync,
			success: function(data) {
				if(isAsync) {
					var times = 0;
					while(times < initList.length) {
						//為防止非同步載入資料順序錯亂,只有songID對應時才新增mp3Url連結
						if(initList[startindex].songID != data.data[0].id) {
							startindex = (startindex + 1) % initList.length;
							times++;
						} else {
							initList[startindex].mp3Url2 = data.data[0].url;
							startindex = (startindex + 1) % initList.length;
							break;
						}
					}
				} else {
					initList[goalIndex].mp3Url2 = data.data[0].url;
				}
			},
			error: function(xhr, status, error) {
				console.log(xhr.status);
				//errorCount++;
			}
		});
	}
}

初始化播放狀態的程式碼為:

//當前播放器狀態
var playStatus = {
	currentTrackLen: 0,
	currentTrackIndex: 0,
	currentTime: 0,
	currentTotalTime: 0,
	playTimes: 0, //點選上一首下一首的次數
	//使用下劃線字首表示這是受保護的物件,即protected
	_playStatus: false,
};

function initPlayStatus() {
	if(currentPlayList) {
		playStatus.currentTrackLen = currentPlayList.length;
	} else {
		playStatus.currentTrackLen = 0;
	}
	playStatus.currentTrackIndex = 0;
	playStatus.currentTime = 0;
	//因為timgTask會不斷讀取音樂的currentTime,
	//當點選本地資料夾音樂列表呼叫initPlayStatus()時,timgTask還會繼續讀取,
	//導致currentTime > currentTotalTime,誤以為當前歌曲已經播放完成,
	//從而不斷地播放一下首,導致bug
	playStatus.currentTotalTime = 10000000;
	playStatus.playTimes = 0;
	playStatus._playStatus = false;
}

初始化前端介面程式碼為:

//播放器控制方法
	playerControls = {
		//歌曲基本資訊
		trackInfo: function(args) {
			//儲存現在正在播放的歌曲
			currentPlaySong = args;

			//先新增audio元素
			$('#audio').remove();
			$('.player').append('<audio id="audio" preload="auto"><source  id="source1" src=""><source id="source2" src=""></source></audio>');
			$('#source1').attr('src', args.mp3Url2);
			$('#source2').attr('src', args.mp3Url);

			//載入audio音訊
			$("#audio")[0].load();

			if(args.isLocal) {
				//如果是本地資料夾的歌曲,則在audio音訊元素載入完成時進行讀取音訊時長
				$("#audio")[0].onloadedmetadata = function() {
					//音訊時長單位為秒
					var totalTime = $("#audio")[0].duration;
					$('.player .time .total').text(timeConvert(totalTime));
					playStatus.currentTotalTime = totalTime - 1;
					args.totalTime = totalTime;
				}
			} else {
				//網易雲介面返回的音訊時長單位為毫秒
				//顯示這首歌的時長,
				$('.player .time .total').text(timeConvert(args.totalTime / 1000));
				//減1是為了避免計算誤差,無法自動下一首
				playStatus.currentTotalTime = Math.floor(args.totalTime / 1000) - 1;
			}

			//根據歌名長度設定字型大小,避免歌名太長超出邊框
			$('.player .trackInfo .name p').text(args.songName);
			if(args.songName.length >= 40) {
				$('.player .trackInfo .name').css("font-size", "18px");
			} else if(args.songName.length > 30) {
				$('.player .trackInfo .name').css("font-size", "22px");
			} else {
				$('.player .trackInfo .name').css("font-size", "26px");
			}
			//歌手名稱
			$('.player .trackInfo .artist').text(args.artist);
			//歌曲圖片
			if(args.isLocal) {
				$('.player .albumPic').css('background', 'url(img/artist.jpg)');
			} else {
				$('.player .albumPic').css('background', 'url(' + args.albumPic + ')');
			}

			//獲取歌詞
			getLyric();
		}
	}	

播放音樂程式碼為:

(主要是做了很多網路載入資源出錯或mp3連結失效的處理)

//播放、暫停狀態處理
		playStatus: function() {
			$('.player .controls .play i').attr('class', 'icon-' + (playStatus.playStatus ? 'pause' : 'play'));

			if(playStatus.playStatus) {
				//networkState = 3則說明音樂mp3URl連結失效,找不到資源,需要重新獲取歌曲url連結
				//networkState = 0則說明該音樂的mp3Url為空
				if($("#audio")[0].networkState != 3 && $("#audio")[0].networkState != 0) {
					
					$("#audio")[0].play();

					//如果5秒後還是沒有任何資源(reayState=0)而且網路仍然在載入(networkState=2)
					//則重新載入重新整理歌曲,重新載入次數不超過3次,仍然失敗則是網路問題
					setTimeout(function() {
						console.log("readyState:" + $("#audio")[0].readyState);
						console.log("networkState:" + $("#audio")[0].networkState);
						console.log("error:" + $("#audio")[0].error);
						if($("#audio")[0].readyState == 0 &&
							$("#audio")[0].networkState == 2) {
							//重新重新整理的歌曲資訊
							playerControls.trackInfo(currentPlaySong);
							$("#audio")[0].load();
							loadTimes++;
							//playStatus.playStatus = false;
							//先停頓0.1秒,讓程式先載入音樂資源,否則networkState屬性會為3,意為找不到資源
							if(loadTimes <= 4) {
								setTimeout(function() {
									playerControls.playStatus();
								}, 300);
							} else {
								loadTimes = 0;
								playStatus.playStatus = false;
								alert("載入歌曲失敗,請檢查網路。");
							}
						} else if($("#audio")[0].networkState == 3) {
							//如果5秒後該音樂的網路狀態為3,則說明請求資源失效
							loadTimes = 0;
							invalidTimes++; //歌曲失效次數增加,超過3次重新整理url連結
							playerControls.trackInfo(currentPlaySong);
							$("#audio")[0].load();
							//呼叫播放方法去重新整理連結
							playerControls.playStatus();
						}
					}, 5000);

					//當點選下一首上一首的次數超過8次,對url進行更新
					//因為指定了ajax使用cached,已經載入url的不用重新連線網易雲介面
					if(playStatus.playTimes >= 8 && currentPlayListIndex != 2) {
						init20Songs();
						playStatus.playTimes = 0;
					}

					//顯示第一句歌詞
					if(lyricResult.length <= 0) {
						$("#lyric").text("純音樂,請欣賞。");
					} else {
						$("#lyric").text(lyricResult[0][1]);
					}

				} else {
					//先判斷是否已經獲取了3次
					if(currentPlaySong.connectTimes < 3) {
						//同步獲取當前失效歌曲的url連結
						if(currentPlayListIndex == 1) {
							//如果是網路搜尋結果則更新songSearchResults
							getMp3Url(songSearchResults, 0, 1, playStatus.currentTrackIndex, false);
							songSearchResults[currentPlaySong.index].connectTimes++;
							playStatus.playStatus = false;
							//重新載入失效的歌曲資訊
							playerControls.trackInfo(songSearchResults[currentPlaySong.index]);
						} else if(currentPlayListIndex == 0) {
							//否則更新musicInfos
							getMp3Url(musicInfos, 0, 1, playStatus.currentTrackIndex, false);
							musicInfos[playStatus.currentTrackIndex].connectTimes++;
							playStatus.playStatus = false;
							//重新載入失效的歌曲資訊
							playerControls.trackInfo(musicInfos[playStatus.currentTrackIndex]);
						}
						$("#audio")[0].load();
						//先停頓0.1秒,讓程式先載入音樂資源,否則networkState屬性會為3,意為找不到資源
						setTimeout(function() {
							playerControls.playStatus();
						}, 300);
						alert("歌曲連結失效,請重新點選播放鍵。");
					} else {
						playStatus.playStatus = false;
						alert("抱歉,該歌曲獲取失敗,請換下一首歌。");
					}

					//失效次數超過3次則說明快取的url連結幾乎都失效了,需要重新獲取,重新整理快取
					invalidTimes++;
					if(invalidTimes >= 3 && currentPlayListIndex != 2) {
						init20Songs(false, false);
						invalidTimes = 0;
					}
				}
			} else {
				if($("#audio")[0].played) {
					$('#audio')[0].pause();
					//恢復歌詞字樣
					if(lyricResult.length <= 0){
						$("#lyric").text("純音樂,請欣賞。");
					}else{
						$("#lyric").text("歌詞");
					}
					
				}
			}
		}

播放進度條和載入動畫的程式碼為:

var timeOut;
//啟動定時任務,載入時間進度條和顯示載入動畫
function timingTask() {
	//因為interval會有累積效應,比如alert一個視窗,使用者很久都還沒點選,
	//這時執行緒因為alert而堵塞,但interval仍然會計算時間,
	//將到時間但還沒執行的操作新增到佇列中。
	//當用戶點選後,累積的interval就會一次性按順序執行,
	//此時一個clearInterval便無法把所有的interval停止了。
	//因此會造成效能問題。
	//因此時間間隔比較小的儘量使用內嵌setTimeOut來代替。
	//注意:setTimeOut也會累積
	playStatus.currentTime = $('#audio')[0].currentTime;
	playerControls.playTime();

	if(playStatus.currentTime >= playStatus.currentTotalTime) {
		$('.player .controls .next').click();
	}

	//根據網路狀態顯示載入動畫
	//readyState == 0 表示have-nothing,還沒開始載入資源
	//readyState == 2 表示已經有當前資料,但是還沒有下面播放的資料
	//readyState == 4 表示已經有足夠的資料
	//networkState == 2 表示請求網路資源正在載入中
	//networkState == 1 表示已經請求到網路資源,可以播放了
	if(($("#audio")[0].readyState != 4) &&
		$("#audio")[0].networkState != 1) {
		if($(".loader:first").css("display") == "none") {
			$(".loader:first").css("display", "block");
		}
	} else {
		if($(".loader:first").css("display") == "block") {
			$(".loader:first").css("display", "none");
		}
	}

	//如果playStatus為true則迴圈呼叫自己,
	//否則將自己停止
	if(playStatus.playStatus) {
		timeOut = setTimeout(timingTask, 300);
	} else {
		clearTimeout(timeOut);
		//如果載入動畫還在載入,則停止載入
		if($(".loader:first").css("display") == "block") {
			$(".loader:first").css("display", "none");
		}
		//重新整理播放按鈕狀態
		$('.player .controls .play i').attr('class', 'icon-' + (playStatus.playStatus ? 'pause' : 'play'));
	}
}

//使用definedProperty方法監聽playStatus屬性,當為true的時候才進行定時任務
Object.defineProperty(playStatus, 'playStatus', {
	get: function() {
		return this._playStatus;
	},
	set: function(value) {
		this._playStatus = value;
		if(value == true) {
			//開啟定時任務
			timingTask();
		} else {
			//按了停止鍵,則停止定時任務
			clearTimeout(timeOut);
			//如果載入動畫還在載入,則停止載入
			if($(".loader:first").css("display") == "block") {
				$(".loader:first").css("display", "none");
			}
			//重新整理播放按鈕狀態
			$('.player .controls .play i').attr('class', 'icon-' + (playStatus.playStatus ? 'pause' : 'play'));
		}
	}
});

播放電腦本地音樂的程式碼為:

function getLocalFiles() {
	//如果本地音樂列表不存在,則進行選擇音樂檔案
	if(localMusicFiles.length <= 0 || $("#localMusicList").css("display") == "block") {
		if($("#localMusic").length != 0) {
			$("#localMusic").fadeIn(200);
		} else {
			$("body").append('<div id="localMusic">' +
				'<div id="music_top">請選擇您的音樂檔案:' +
				'<div class="music_cross"><span class="icon-cross3"></span></div>' +
				'<br /> <span style="font-size:14px">(可以一次性選多首音樂)</span>' +
				'</div>' +
				'<div id="music_cont"><input id="musicFiles" class="musicInput"  type="file" accept="audio/*" multiple/></div>' +
				'<div class="music_confirm" id="music_confirm"><span class="icon-checkmark"></span></div>' +
				'</div>');      
		}

		$(".icon-cross3").unbind("click").click(function() {
			$("#localMusic").fadeOut(200);
		});

		$("#music_confirm").unbind("click").click(function() {
			var musicFiles = document.getElementById("musicFiles");
			//value屬性儲存的是檔案的絕對路徑,如果為空則說明沒有選到檔案
			if(musicFiles.value) {
				$("#localMusic").fadeOut(200);
				var files = musicFiles.files;
				var hasNotEmpty = true;
				var isMp3 = false;

				for(var i = 0; i < files.length; i++) {
					//如果選擇的檔案是音訊檔案才進行新增
					if(/audio\/\w+/g.test(files[i].type)) {

						//存在音訊檔案才把上次選擇的本地音樂清空,再進行新增
						if(hasNotEmpty) {
							localMusicFiles = [];
							hasNotEmpty = false;
							isMp3 = true;
						}

						var musicInfo = new Object();

						musicInfo.songID = -1;
						musicInfo.songName = files[i].name;
						musicInfo.artist = "本地歌曲";
						musicInfo.albumPic = "";
						//本地歌曲時長時間還需要獲取資料處理
						musicInfo.totalTime = 210000;
						musicInfo.mp3Url = "";
						musicInfo.mp3Url2 = "";
						musicInfo.connectTimes = 0;
						musicInfo.index = i;
						//先將本地音樂檔案的files物件儲存,等到使用者點選歌曲的時候才進行載入播放
						musicInfo.musicData = files[i];
						//標誌該歌曲是本地歌曲
						musicInfo.isLocal = true;
						localMusicFiles.push(musicInfo);
					}
				}

				//如果選擇的所有檔案都不是音訊檔案則進行提醒
				if(!isMp3) {
					alert("請選擇音訊檔案");
					$("#localMusic").fadeIn(0);
				} else {
					//將本地音樂資料傳送到本地音樂列表頁面
					window.frames[4].postMessage(localMusicFiles, "/");
					$("#musicList").css("display", "none");
					$("#songList").css("display", "none");
					$("#searchMusicList").css("display", "none");
					$("#netSearchList").css("display", "none");
					$("#localMusicList").css("display", "block");
				}
			} else {
				alert("請選擇音樂檔案");
			}
		});
	} else {
		//如果本地音樂檔案列表已經存在,則直接顯示出來
		listButtonShow(true);
		$(".musicList").css("display", "none");
		$("#title").css("display", "none");
		$("#search").css("display", "block");
		$("#local").css("display", "block");
		$("#localMusicList").css("display", "block");
	}
}

解析歌詞的程式碼為:

//呼叫trackInfo方法載入音樂資訊的時候,也進行載入歌詞,並進行解析
function getLyric() {
	if(currentPlaySong.isLocal) {
		//如果是本地音樂,則把歌詞隱藏
		$(".lyric").css("display", "none");
	} else {
		//如果是網路音樂,則把歌詞顯示
		$(".lyric").css("display", "block");
		//通過ajax非同步獲取歌詞資訊,如果網路異常則給出提示
		$.ajax({
			url: lyricApi + currentPlaySong.songID,
			type: "get",
			dataType: 'JSON',
			async: true,
			cache: true,
			success: processLyric,
			error: function(xhr, status, error) {
				alert("抱歉,獲取歌詞失敗,伺服器出故障了。");
			}
		});
	}
}

//解析歌詞
function processLyric(lyricData) {
	//正則表示式,匹配[00:59.40]這是時間格式
	var pattern = /\[\d{2}:\d{2}.\d+\]/g;
	
	//如果標誌沒有歌詞或者歌詞內容不匹配時間格式,則歸為純音樂
	if(lyricData.nolyric == true || !pattern.test(lyricData.lrc.lyric)) {
		lyricResult = [];
		lyricArtistMessage = [];
		$("#lyricDiv").empty();
		$("#lyricDiv").append("<ul><li>純音樂,請欣賞。</li></ul>")
		$("#lyric").text("純音樂,請欣賞。");
		return;
	}

	var lyricInfo = lyricData.lrc.lyric;
	var lines = lyricInfo.split("\n");

	//用來儲存解析出來的歌詞,格式如下[[time,lyricString],[time,lyricString],.....]
	lyricResult = [];
	lyricArtistMessage = [];

	//有一些歌詞前面時歌手的介紹,沒有歌詞的,需要把它去掉
	//直到匹配到有時間的歌詞才會停止迴圈
	var artistPattern = /\[\w+:/g;
	
	//需要注意,正則表示式使用g模式的話,下一次匹配會從lastIndex開始匹配,
	//因為上面使用了pattern進行了匹配,有可能lastIndex不為0,所以需要重置為0
	
	pattern.lastIndex  = 0;
	
	while(!pattern.test(lines[0])) {
		//先把歌手資訊儲存下來,因為全屏歌詞的時候需要顯示
		var lyricMessage = lines[0].replace(artistPattern, "").slice(0, -1);
		lyricMessage.length > 0 ? lyricArtistMessage.push(lyricMessage) : 0;

		//去掉第一個元素並返回剩下元素
		lines = lines.splice(1);
	}

	//lines最後一行是空的話把它去除掉
	lines[lines.length - 1].length == 0 && lines.pop();

	lines.forEach(function(value, index, array) {
		//返回匹配的時間
		var time = value.match(pattern);
		//將時間置換成空格,返回歌詞字串內容
		var lyricString = value.replace(pattern, "");
		//由於一行歌詞會有多個時間, 如[03:33.65][03:35.39],所以需要進一步分離
		time.forEach(function(value2, index2, array2) {
			//去掉前後的[]
			var t = value2.slice(1, -1).split(":");
			//用秒數表示當前歌詞的時間
			var seconds = parseInt(t[0]) * 60 + parseFloat(t[1]);
			//將時間和歌詞以陣列的形式壓進lyriclyricResult中
			lyricResult.push([seconds, lyricString]);
		});
	});

	//將結果按照時間排序,保證歌詞正確有序輸出
	lyricResult.sort(function(a, b) {
		//如果想要a在b前面,則返回一個負數,否則a想排在b後面,則返回一個正數
		//所以想要元素按照升序排序,則返回a與b的差值就行
		return a[0] - b[0];
	});

	addLyric();
	//把歌詞位置置為初始化
	lyricIndex = 1;
	showLyric();
}

function addLyric() {
	$("#lyricDiv").empty();
	var ul = $("<ul></ul>");
	//歌手資訊
	for(var i = 0; i < lyricArtistMessage.length; i++) {
		ul.append("<li>" + lyricArtistMessage[i] + "</li>")
	}
	//歌詞資訊
	for(var i = 0; i < lyricResult.length; i++) {
		ul.append("<li>" + lyricResult[i][1] + "</li>")
	}
	$("#lyricDiv").append(ul);
}

//監聽audio的timeUpdate事件,
//第一句歌詞會在點選播放按鈕時顯示出來,
//如果當前時間已經大於第一句歌詞的時間,則說明第一句唱完了,
//則把第一句隱藏,更改歌詞內容,顯示第二句歌詞.
//再把當前時間跟第三句歌詞時間進行比較,依次迴圈.

var lyricIndex = 1; //記錄當前是第幾條歌詞
function showLyric() {

	//因為歌詞頭部還有歌手資訊,因此高亮的歌詞從j開始
	var j = lyricArtistMessage.length;
	$("#lyricDiv ul li").get(j).style.color = "#ff6666";

	//檢測第第三句歌詞,如果是中文,則歌詞大小改為18px
	if(lyricResult.length > 3) {
		for(var i = 0; i < lyricResult[2][1].length; i++) {
			//因為歌詞英文的分號為’,所以也要排除這個
			if(lyricResult[2][1].charCodeAt(i) > 127 && lyricResult[2][1].charAt(i) != '’') {
				$("#lyricDiv ul").css("font-size", "19px");
				$("#lyric").css("font-size", "19px");
			}
		}
	}

	$("#audio")[0].ontimeupdate = function(e) {

		if(lyricResult.length > 0 && this.currentTime > lyricResult[lyricIndex][0]) {
			//如果大屏歌詞沒有開啟,則小框進行顯示
			if($("#lyricDiv").css("display") == "none") {
				$("#lyric").fadeOut(0);
				$("#lyric").text(lyricResult[lyricIndex][1]);
				$("#lyric").fadeIn();
			} else {
				$("#lyric").text("歌詞");

				//把上一條歌詞顏色進行恢復
				$("#lyricDiv ul li").css("color", "#cbc7c7");
				//把現在這條歌詞高亮顯示
				$("#lyricDiv ul li").get(lyricIndex + j).style.color = "#ff6666";

				//初始歌詞的高度是80,所以將80-疊加的高度則得出歌詞需要滑動的高度
				$("#lyricDiv ul").animate({
					"top": 80 - lyricHeight[lyricIndex - 1] + "px"
				}, 1000);
			}

			//把歌詞的索引移到下一條
			lyricIndex++;
		}
	}
}

function showFullLyric() {
	$(".header button").css("display", "none");
	$("#back").css("display", "block");
	$(".musicList").css("display", "none");
	$(".albumPic").css("display", "none");
	$("#lyricDiv").css("display", "block");

	//把所有歌詞的高度儲存,因為設為none之後該值也會丟失
	//為了方面後面設定高度,儲存的值是疊加高度的值
	lyricHeight = [];
	for(var i = 0; i < $("#lyricDiv ul li").length; i++) {
		var last = i == 0 ? 0 : lyricHeight[i - 1];
		lyricHeight.push(last + $("#lyricDiv ul li")[i].offsetHeight);
	}

	$("#lyric").text("歌詞");

	//迅速滑到當前播放的歌詞
	//初始歌詞的高度是80,所以將80-疊加的高度則得出歌詞需要滑動的高度
	if(lyricIndex >= 2){
		$("#lyricDiv ul").animate({
			"top": 80 - lyricHeight[lyricIndex - 1] + "px"
		}, 1000);
	}
}

function lyricTip(t, e) {

	var lyric = document.getElementsByClassName("lyric")[0];
	var tooltipTop = lyric.offsetTop;

	if(e.target.id == "lyric") {
		var tooltipHtml = "<div id='tooltip' class='tooltip'>點選檢視全部歌詞 </div>";
		$(t).append(tooltipHtml); //新增到頁面中  
		$("#tooltip").css({
			"top": tooltipTop + 30 + "px",
			"left": "210px",
		}).show("fast"); //設定提示框的座標,並顯示 
	}
}

畫星空背景的程式碼為:

//畫星空背景
function drawStars() {
	var canvas = document.getElementById('canvas'),
		ctx = canvas.getContext('2d'),
		w = canvas.width = window.innerWidth,
		h = canvas.height = window.innerHeight,

		hue = 217, //色調色彩
		stars = [], //儲存所有星星
		count = 0,  //用於計算星星
		maxStars = 1300; //星星數量
	
	//canvas2是用來建立星星的源影象,即母版,
	//根據星星自身屬性的大小來設定
	var canvas2 = document.createElement('canvas'),
		ctx2 = canvas2.getContext('2d');
	canvas2.width = 100;
	canvas2.height = 100;
	//建立徑向漸變,從座標(half,half)半徑為0的圓開始,
	//到座標為(half,half)半徑為half的圓結束
	var half = canvas2.width / 2,
		gradient2 = ctx2.createRadialGradient(half, half, 0, half, half, half);
	gradient2.addColorStop(0.025, '#CCC');
	//hsl是另一種顏色的表示方式,
	//h->hue,代表色調色彩,0為red,120為green,240為blue
	//s->saturation,代表飽和度,0%-100%
	//l->lightness,代表亮度,0%為black,100%位white
	gradient2.addColorStop(0.1, 'hsl(' + hue + ', 61%, 33%)');
	gradient2.addColorStop(0.25, 'hsl(' + hue + ', 64%, 6%)');
	gradient2.addColorStop(1, 'transparent');

	ctx2.fillStyle = gradient2;
	ctx2.beginPath();
	ctx2.arc(half, half, half, 0, Math.PI * 2);
	ctx2.fill();

	// End cache
	function random(min, max) {
		if(arguments.length < 2) {
			max = min;
			min = 0;
		}

		if(min > max) {
			var hold = max;
			max = min;
			min = hold;
		}
		
		//返回min和max之間的一個隨機值
		return Math.floor(Math.random() * (max - min + 1)) + min;
	}

	function maxOrbit(x, y) {
		var max = Math.max(x, y),
			diameter = Math.round(Math.sqrt(max * max + max * max));
		//星星移動範圍,值越大範圍越小,
		return diameter / 2;
	}

	var Star = function() {
		//星星移動的半徑
		this.orbitRadius = random(maxOrbit(w, h));
		//星星大小,半徑越小,星星也越小,即外面的星星會比較大
		this.radius = random(60, this.orbitRadius) / 8;
		//所有星星都是以螢幕的中心為圓心
		this.orbitX = w / 2;
		this.orbitY = h / 2;
		//星星在旋轉圓圈位置的角度,每次增加speed值的角度
		//利用正弦餘弦算出真正的x、y位置
		this.timePassed = random(0, maxStars);
		//星星移動速度
		this.speed = random(this.orbitRadius) / 50000;
		//星星影象的透明度
		this.alpha = random(2, 10) / 10;

		count++;
		stars[count] = this;
	}

	Star.prototype.draw = function() {
		//星星圍繞在以螢幕中心為圓心,半徑為orbitRadius的圓旋轉
		var x = Math.sin(this.timePassed) * this.orbitRadius + this.orbitX,
			y = Math.cos(this.timePassed) * this.orbitRadius + this.orbitY,
			twinkle = random(10);
		
		//星星每次移動會有1/10的機率變亮或者變暗
		if(twinkle === 1 && this.alpha > 0) {
			//透明度降低,變暗
			this.alpha -= 0.05;
		} else if(twinkle === 2 && this.alpha < 1) {
			//透明度升高,變亮
			this.alpha += 0.05;
		}

		ctx.globalAlpha = this.alpha;
		//使用canvas2作為源影象來建立星星,
		//位置在x - this.radius / 2, y - this.radius / 2
		//大小為 this.radius
		ctx.drawImage(canvas2, x - this.radius / 2, y - this.radius / 2, this.radius, this.radius);
		//沒旋轉一次,角度就會增加
		this.timePassed += this.speed;
	}
	
	//初始化所有星星
	for(var i = 0; i < maxStars; i++) {
		new Star();
	}

	function animation() {
		//以新影象覆蓋已有影象的方式進行繪製背景顏色
		ctx.globalCompositeOperation = 'source-over';
		ctx.globalAlpha = 0.5; //尾巴
		ctx.fillStyle = 'hsla(' + hue + ', 64%, 6%, 2)';
		ctx.fillRect(0, 0, w, h)

		//源影象和目標影象同時顯示,重疊部分疊加顏色效果
		ctx.globalCompositeOperation = 'lighter';
		for(var i = 1, l = stars.length; i < l; i++) {
			stars[i].draw();
		};
		
		//呼叫該方法執行動畫,並且在重繪的時候呼叫指定的函式來更新動畫
		//回撥的次數通常是每秒60次
		window.requestAnimationFrame(animation);
	}

	animation();
}
  • 還有音樂列表歌名搜尋、網路音樂搜尋、自定義div彈出框、列表資訊顯示、iframe跨域傳送資訊、播放快捷鍵繫結等功能,在index.js程式碼檔案中都有很詳細的註釋,可以自行檢視。

後序

  • 其實在寫播放器的時候,是沒有一個整體的框架的,基本都是想到什麼功能,就新增一個函式,幾乎所有指令碼程式碼都寫在一個index.js檔案中了。這也是後續學習的方向,畢竟軟體效能、程式碼維護都需要良好的框架支撐。
  • 第一次寫GitHub,陳述不清楚,還望見諒,繼續學習中…
  • Stay Hungry,Stay Foolish. Continue Hello World!