網絡音樂播放器的實現(Flex+JAVA)


最近在做一個音樂播放器,純粹練手,前端使用FLex,后台使用JAVA,現在慢慢的在實現,主要涉及的技術還在不斷學習中:
這里也一點一點記錄下來和大家分享哈。

自定義組件:(左邊是一個播放列表,右邊是音樂播放控件)
自定義組件要繼承SkinnableComponent類。主要有兩部分組成,一個是組件的功能邏輯,一個是皮膚。
功能邏輯和普通的as寫法一樣,要用到皮膚就需要遵守皮膚的契約.看看下面的代碼:
(音樂播放控件:PlayerControlBar.as):

  1 package components
  2 {
  3     import events.PlayEvent;
  4     import events.StopEvent;
  5     
  6     import flash.events.Event;
  7     import flash.media.Sound;
  8     import flash.media.SoundChannel;
  9     import flash.net.URLRequest;
 10     
 11     import mx.controls.Alert;
 12     import mx.controls.HSlider;
 13     import mx.controls.sliderClasses.Slider;
 14     import mx.events.SliderEvent;
 15     import mx.messaging.AbstractConsumer;
 16     
 17     import service.IPlayController;
 18     import service.impl.PlayController;
 19     
 20     import spark.components.Button;
 21     import spark.components.TextArea;
 22     import spark.components.supportClasses.SkinnableComponent;
 23     
 24     [SkinState("stop")]
 25     [SkinState("run")]
 26     /**播放控制欄組件*/
 27     public class PlayerControlBar extends SkinnableComponent
 28     {
 29         [SkinPart(required="true")]
 30         public var lyricText:TextArea;
 31         [SkinPart(required="true")]
 32         public var playSlider:HSlider;
 33         [SkinPart(required="true")]
 34         public var preButton:Button;
 35         [SkinPart(required="true")]
 36         public var stateButton:Button;
 37         [SkinPart(required="true")]
 38         public var nextButton:Button;
 39         [SkinPart(required="true")]
 40         public var stopButton:Button;
 41         
 42         public function PlayerControlBar()
 43         {
 44             super();
 45             //添加播放狀態更改的監聽器
 46             this.addEventListener(PlayEvent.PLAY, handleStateButtonClick);
 47             this.addEventListener(StopEvent.STOP, handleStopButtonClick);
 48             this.addEventListener(SliderEvent.CHANGE, handlePlaySilderChange);
 49         }
 50         
 51         /**是否在播放*/
 52         public var isStart:Boolean = false;
 53         /**音樂播放控制器*/
 54         private var playController:IPlayController;
 55         /**播放狀態改變的處理函數*/
 56         private function handleStateButtonClick(event:PlayEvent):void
 57         {
 58             if(!isStart)
 59             {
 60                 //加載音樂並開始播放
 61                 playController = new PlayController(this);
 62                 playController.start("gole.mp3");
 63                 isStart = true;
 64                 //改變皮膚的狀態
 65                 this.skin.currentState="stop";
 66             }
 67             else if(this.skin.currentState == "stop")
 68             {
 69                 //暫停播放音樂
 70                 playController.pause();
 71                 this.skin.currentState="run";
 72             }
 73             else if(this.skin.currentState == "run")
 74             {
 75                 //開始音樂播放
 76                 playController.play();
 77                 this.skin.currentState="stop";
 78             }
 79         }
 80         
 81         private function handleStopButtonClick(e:StopEvent):void
 82         {
 83             isStart = false;
 84             this.skin.currentState = "run";
 85             if(playController)
 86                 playController.stop(true);
 87         }
 88         
 89         //活動條拉動的觸發函數,從指定位置開始播放
 90         private function handlePlaySilderChange(e:SliderEvent):void
 91         {
 92             if(isStart)
 93             {
 94                 (playController as PlayController).clickPlay();
 95                 if(this.skin.currentState == "run")
 96                     this.skin.currentState = "stop";
 97             }
 98         }
 99     }
100 }

看到24~25行為自定義組件加了兩個SkinState標注,這個是皮膚的狀態,
第29~40行為幾個組件加了[SkinPart(required="true")]標注,這個是皮膚必須要擁有的控件
皮膚的契約如下圖:

利用flex builder的功能可以為自定義組件添加皮膚,它會根據皮膚的契約自動生成提示:
如下:
(音樂播放控件的皮膚:PlayerControlBarSkin.mxml):

 1 <?xml version="1.0" encoding="utf-8"?>
 2 <s:Skin xmlns:fx="http://ns.adobe.com/mxml/2009" 
 3         xmlns:s="library://ns.adobe.com/flex/spark" 
 4         xmlns:mx="library://ns.adobe.com/flex/mx"
 5         creationComplete="this.currentState = 'run'"
 6         >
 7     <!-- host component -->
 8     <fx:Metadata>
 9         [HostComponent("components.PlayerControlBar")]
10     </fx:Metadata>
11     <fx:Script>
12         <![CDATA[
13             import events.PlayEvent;
14             import events.StopEvent;
15             
16             import mx.events.SliderEvent;
17         ]]>
18     </fx:Script>
19     
20     <!-- states -->
21     <s:states>
22         <s:State id="runState" name="run"/>
23         <s:State id="stopState" name="stop" />
24     </s:states>
25     
26     <!-- SkinParts
27     name=lyricText, type=spark.components.TextArea, required=true
28     name=stateButton, type=spark.components.Button, required=true
29     name=nextButton, type=spark.components.Button, required=true
30     name=preButton, type=spark.components.Button, required=true
31     name=stopButton, type=spark.components.Button, required=true
32     -->
33     
34     <s:Group width="700" height="600">
35         <s:Image source="../asserts/img/background.jpg" alpha=".6"/>
36         
37         <s:VGroup width="100%" height="100%" horizontalAlign="center" paddingTop="20">
38             <s:Group width="60%" height="80%" horizontalCenter="0">
39                 <s:TextArea id="lyricText" width="100%" height="100%" alpha=".8" borderVisible="false">
40                 </s:TextArea>
41             </s:Group>
42             <s:HGroup width="55%" verticalAlign="middle">
43                 <mx:HSlider id="playSlider" width="100%" height="100%" minimum="0" maximum="100"
44                             change="dispatchEvent(new SliderEvent(SliderEvent.CHANGE,true))"/>
45             </s:HGroup>
46             <s:HGroup width="60%" horizontalAlign="center" paddingBottom="10">
47                 <s:Button id="preButton" skinClass="skins.PreButtonSkin"/>
48                 <s:Button left="15" id="stateButton" skinClass.run="skins.PlayButtonSkin" skinClass.stop="skins.PauseButtonSkin" click="dispatchEvent(new PlayEvent(PlayEvent.PLAY))"/>
49                 <s:Button left="15" id="nextButton" skinClass="skins.NextButtonSkin"/>
50                 <s:Button left="15" id="stopButton" skinClass="skins.StopButtonSkin" click="dispatchEvent(new StopEvent(StopEvent.STOP))"/>
51             </s:HGroup>
52         </s:VGroup>
53     </s:Group>
54 </s:Skin>

自定義組件的好處是將邏輯內部封裝好,安全也便於維護,通過皮膚改變外觀也是很方便的,需要對外的服務只要提供接口就可以了。

在主界面使用自定義組件:

1 <s:HGroup horizontalAlign="center" verticalAlign="middle" horizontalCenter="0" verticalCenter="0">
2         <components:MusicList skinClass="skins.MusicListSkin" listContent="{musicList}" creationComplete="initMusicList()"/>        
3         <components:PlayerControlBar skinClass="skins.PlayeControlBarSkin"/>
4 </s:HGroup>

運行效果(自定義組件PlayerControlBar)

 

自定義事件和事件派發:

事件流有三個階段:捕獲階段--->目標階段--->冒泡階段
1.捕獲階段(從根節點到子節點,檢測對象是否注冊了監聽器,是則調用監聽函數)
2.目標階段(調用目標對象本身注冊的監聽程序)
3.冒泡階段(從目標節點到根節點,檢測對象是否注冊了監聽器,是則調用監聽函數)
注:事件發生后,每個節點可以有2個機會(2選1)響應事件,默認關閉捕獲階段。
從上到下(從根到目標)是捕獲階段,到達了目標后是目標階段,然后從目標向上返回是冒泡階段。

這里需要注意的是:如果派發事件的源(調用dispatchEvent方法)沒有在一組容器里,那么這組容器里面的控件是監聽不到這個派發事件的。

如下,在點擊stateButton按鈕的時候會派發一個自定義的事件PlayEvent, 然后在自定義組件PlayControlBar中添加
監聽並作出相應處理:

<s:Button left="15" id="stateButton" skinClass.run="skins.PlayButtonSkin" skinClass.stop="skins.PauseButtonSkin" click="dispatchEvent(new PlayEvent(PlayEvent.PLAY))"/>

(自定義的事件:PlayEvent.as) :

 1 package events
 2 {
 3     import flash.events.Event;
 4     
 5     import mx.states.OverrideBase;
 6     
 7     public class PlayEvent extends Event
 8     {
 9         public static const PLAY:String = "play";
10         
11         public function PlayEvent(type:String = "play", bubbles:Boolean=true, cancelable:Boolean=false)
12         {
13             super(type, bubbles, cancelable);
14         }
15         
16         override public function clone():Event
17         {
18             return new PlayEvent();
19         }
20     }
21 }

自定義事件要繼承Event類和重寫clone方法。構造函數的第二參數表示是否要執行冒泡,如果不冒泡,父容器就捕獲不到事件

添加監聽:

public function PlayerControlBar()
        {
            super();
            //添加播放狀態更改的監聽器
            this.addEventListener(PlayEvent.PLAY, handleStateButtonClick);
          ………………
        }

事件處理函數:

 1 private function handleStateButtonClick(event:PlayEvent):void
 2         {
 3             if(!isStart)
 4             {
 5                 //加載音樂並開始播放
 6                 playController = new PlayController(this);
 7                 playController.start("gole.mp3");
 8                 isStart = true;
 9                 //改變皮膚的狀態
10                 this.skin.currentState="stop";
11             }
12             else if(this.skin.currentState == "stop")
13             {
14                 //暫停播放音樂
15                 playController.pause();
16                 this.skin.currentState="run";
17             }
18             else if(this.skin.currentState == "run")
19             {
20                 //開始音樂播放
21                 playController.play();
22                 this.skin.currentState="stop";
23             }
24         }

 

音頻播放的處理:
Flex中音頻的播放主要是靠Sound和SoundChannel兩個類來實現的。
具體的使用Adobe的官方文檔講得非常詳細,地址是:
http://help.adobe.com/zh_CN/FlashPlatform/reference/actionscript/3/flash/media/Sound.html

(控制音樂播放類:PlayController.as):

  1 package service.impl
  2 {
  3     import components.PlayerControlBar;
  4     
  5     import flash.display.DisplayObject;
  6     import flash.events.Event;
  7     import flash.events.TimerEvent;
  8     import flash.media.Sound;
  9     import flash.media.SoundChannel;
 10     import flash.net.URLRequest;
 11     import flash.utils.Timer;
 12     
 13     import mx.managers.CursorManager;
 14     
 15     import service.IPlayController;
 16     
 17     /**音樂播放控制類*/
 18     public class PlayController implements IPlayController
 19     {
 20         private var sound:Sound;
 21         private var soundChannel:SoundChannel;
 22         private var _pausePosition:int;
 23         private var _derectory:String =  "../music/"
 24         /**實時記錄播放進度*/
 25         private var timer:Timer;
 26         private var view:PlayerControlBar;
 27         
 28         public function PlayController(view:DisplayObject)
 29         {
 30             this.view = view as PlayerControlBar;
 31         }
 32         
 33 
 34         /**音樂播放暫停位置*/
 35         public function get pausePosition():int
 36         {
 37             return _pausePosition;
 38         }
 39 
 40         /**
 41          * @private
 42          */
 43         public function set pausePosition(value:int):void
 44         {
 45             _pausePosition = value;
 46         }
 47 
 48         /**音樂存放的目錄*/
 49         public function get derectory():String
 50         {
 51             return _derectory;
 52         }
 53 
 54         public function set derectory(value:String):void
 55         {
 56             _derectory = value;
 57         }
 58 
 59         public function start(music:String):void
 60         {
 61             sound = new Sound();
 62             timer = new Timer(1000);
 63             var urlRequest:URLRequest = new URLRequest(derectory + music);
 64             sound.addEventListener(Event.COMPLETE, function handleStart(e:Event):void
 65             {
 66                 soundChannel = sound.play();
 67                 //增加音樂播放完畢的監聽器
 68                 soundChannel.addEventListener(Event.SOUND_COMPLETE, handlePlayEnd);
 69                 timer.start();
 70                 sound.removeEventListener(Event.COMPLETE,handleStart);
 71             }
 72             );
 73             timer.addEventListener(TimerEvent.TIMER, handleTimmerWork);
 74             sound.load(urlRequest);
 75         }
 76         
 77         /*音樂播放結束處理函數*/
 78         private function handlePlayEnd(e:Event):void
 79         {
 80             stop(true);
 81             view.skin.currentState = "run";
 82             view.isStart = false;
 83             soundChannel.removeEventListener(Event.SOUND_COMPLETE,handlePlayEnd);
 84         }
 85         
 86         /*每隔一秒,刷新進度條*/
 87         private function handleTimmerWork(e:TimerEvent):void
 88         {
 89             var estimatedLength:int =  Math.ceil(sound.length / (sound.bytesLoaded / sound.bytesTotal)); 
 90             var playbackPercent:uint =  Math.round(100 * (soundChannel.position / estimatedLength));
 91             view.playSlider.value = playbackPercent;
 92         }
 93         
 94         public function pause():void
 95         {
 96             if(soundChannel)
 97             {
 98                 pausePosition = soundChannel.position;
 99                 stop();                    
100             }
101         }
102         
103         public function play():void
104         {
105             if(sound)
106             {
107                 soundChannel = sound.play(pausePosition);
108                 soundChannel.addEventListener(Event.SOUND_COMPLETE, handlePlayEnd);                
109             }
110             if(!timer.running)
111                 timer.start();
112         }
113         
114         public function stop(isExit:Boolean = false):void
115         {
116             if(soundChannel)
117             {
118                 soundChannel.stop();
119                 soundChannel.removeEventListener(Event.SOUND_COMPLETE,handlePlayEnd);                
120             }
121             if(timer.running)
122                 timer.stop();
123             if(isExit)
124                 timer.removeEventListener(TimerEvent.TIMER,handleTimmerWork);
125         }
126         
127         /**由Slider觸發的播放*/
128         public function clickPlay():void
129         {
130             //根據拖動的位置計算實際音樂播放的位置
131             var percent:Number = view.playSlider.value / view.playSlider.maximum;
132             var estimatedLength:uint = Math.ceil(sound.length / (sound.bytesLoaded / sound.bytesTotal));
133             var position:uint = Math.round(percent * estimatedLength);
134             pause();
135             pausePosition = position;
136             play();
137         }
138     }
139 }

第59至75行是第一次點擊播放時調用的方法,第64行Sound增加了一個事件監聽,是在音樂加載完后執行的,
這個如果要邊加載邊播放的時候不適用,可以參考官方文檔來解決這個問題。第94~101行中是點擊暫停時調用
的方法,暫停的時候要把音樂播放的位置記錄下來,如98行,這是為了要在繼續播放的時候找到起點。第103
至112行是繼續播放函數。


Timer類的使用實現播放進度實時更新:


當音樂播放的時候,這個播放進度條會每個同步的移動位置,拖動進度條,音樂也會播放到相應的位置。

進度條控件:

<s:HGroup width="55%" verticalAlign="middle">
                <mx:HSlider id="playSlider" width="100%" height="100%" minimum="0" maximum="100"
                            change="dispatchEvent(new SliderEvent(SliderEvent.CHANGE,true))"/>
</s:HGroup>

當進度條通過拖動或者點擊改變值的時候會派發自定義事件SliderEvent,這個在自定義組件PlayerControlBar中進行監聽和處理.

public function PlayerControlBar()
        {
            super();
           ………………this.addEventListener(SliderEvent.CHANGE, handlePlaySilderChange);
        }

 處理函數:

//進度條拉動的觸發函數,從指定位置開始播放
        private function handlePlaySilderChange(e:SliderEvent):void
        {
            if(isStart)
            {
                (playController as PlayController).clickPlay();
                if(this.skin.currentState == "run")
                    this.skin.currentState = "stop";
            }
        }

clickplay方法:

 1         /**由Slider觸發的播放*/
 2         public function clickPlay():void
 3         {
 4             //根據拖動的位置計算實際音樂播放的位置
 5             var percent:Number = view.playSlider.value / view.playSlider.maximum;
 6             var estimatedLength:uint = Math.ceil(sound.length / (sound.bytesLoaded / sound.bytesTotal));
 7             var position:uint = Math.round(percent * estimatedLength);
 8             pause();
 9             pausePosition = position;
10             play();
11         }

第5~7行是計算當前進度條拖動的進度對應的音樂播放位置。

實時更新播放進度條:

 1 public function start(music:String):void
 2         {
 3             sound = new Sound();
 4             timer = new Timer(1000);
 5             var urlRequest:URLRequest = new URLRequest(derectory + music);
 6             sound.addEventListener(Event.COMPLETE, function handleStart(e:Event):void
 7             {
 8                 soundChannel = sound.play();
 9                 //增加音樂播放完畢的監聽器
10                 soundChannel.addEventListener(Event.SOUND_COMPLETE, handlePlayEnd);
11                 timer.start();
12                 sound.removeEventListener(Event.COMPLETE,handleStart);
13             }
14             );
15             timer.addEventListener(TimerEvent.TIMER, handleTimmerWork);
16             sound.load(urlRequest);
17         }

第4行,在點擊音樂播放的時候新建一個Timer類,並規定1秒執行一次,第11行是音樂播放的時候開始啟動這個timer,第15行
中添加timer觸發的事件。處理函數如下:

/*每隔一秒,刷新進度條*/
        private function handleTimmerWork(e:TimerEvent):void
        {
            var estimatedLength:int =  Math.ceil(sound.length / (sound.bytesLoaded / sound.bytesTotal)); 
            var playbackPercent:uint =  Math.round(100 * (soundChannel.position / estimatedLength));
            view.playSlider.value = playbackPercent;
        }

當停止音樂播放的時候也停止timer,開始播放音樂的時候啟動timer:

 1 public function stop(isExit:Boolean = false):void
 2         {
 3             if(soundChannel)
 4             {
 5                 soundChannel.stop();
 6                 soundChannel.removeEventListener(Event.SOUND_COMPLETE,handlePlayEnd);                
 7             }
 8             if(timer.running)
 9                 timer.stop();
10             if(isExit)
11                 timer.removeEventListener(TimerEvent.TIMER,handleTimmerWork);
12         }
 1 public function play():void
 2         {
 3             if(sound)
 4             {
 5                 soundChannel = sound.play(pausePosition);
 6                 soundChannel.addEventListener(Event.SOUND_COMPLETE, handlePlayEnd);                
 7             }
 8             if(!timer.running)
 9                 timer.start();
10         }

 

前端(FLEX)和服務器端(JAVA)之間的通信:
這個是通過Socket來實現的.
JAVA端的Socket編程若要和Flex端通信並且傳遞對象,就需要用到AMF序列化,這個Adobe為我們實現了,
只需要調用接口就可以了。Adobe的這個框架叫做blazeds在官網可以下載到,為了方便大家,這里給出了
下載地址:blazeds.zip    
BlazeDS開發者指南

java服務端:

 1 public class MusicServer {
 2     private ServerSocket serverSocket;
 3     private Socket clientSocket;
 4     
 5     public MusicServer(int port)
 6     {
 7         try {
 8             serverSocket = new ServerSocket(port);
 9             clientSocket = serverSocket.accept();
10             ClientSocketManager.addClient(clientSocket);
11             MusicListService.sendMusicList(clientSocket);
12         } catch (IOException e) {
13             e.printStackTrace();
14         }
15     }
16     
17     
18     public static void main(String[] args) {
19         new MusicServer(9000);
20     }
21 }

第11行是socket輸入,輸出流處理的類,主要是把服務端里面的所有音樂的文件名發送給客戶端。
ClientSocketManager.java:

 1 public class MusicListService {
 2     
 3     public static void sendMusicList(Socket socket)
 4     {
 5         try {
 6             InputStream input = socket.getInputStream();
 7             OutputStream outputStream = socket.getOutputStream();
 8             Amf3Output amfoutput = new Amf3Output(new SerializationContext());
 9             
10             while(true)
11             {
12                 int index = 0;
13                 byte[] buffer = new byte[100];
14                 StringBuffer requestState = new StringBuffer();
15                 while(-1 != (index = input.read(buffer, 0, buffer.length)))
16                 {
17                     String value = new String(buffer, 0, index);
18                     requestState.append(value);
19                     
20                     ByteArrayOutputStream byteOutput = new ByteArrayOutputStream();
21                     DataOutputStream output = new DataOutputStream(byteOutput);
22                     amfoutput.setOutputStream(output);
23                     MusicList musicList = MusicListGet.getMusicList();
24                     amfoutput.writeObject(musicList);
25                     output.flush();
26                     
27                     byte[] data = byteOutput.toByteArray();
28                     outputStream.write(data);
29                     outputStream.flush();
30                 }
31                 
32                 break;
33             }
34         } catch (IOException e) {
35             e.printStackTrace();
36         }finally
37         {
38             try {
39                 socket.close();
40                 ClientSocketManager.removeClient(socket);
41             } catch (IOException e) {
42                 e.printStackTrace();
43             }
44         }
45     }
46 }

 

 

第8行中導入了Amf3Output類,這個類就是blazeds.zip包下面的,導入到項目中就可以了。


Flex客戶端:

 1 public class SocketController extends EventDispatcher
 2     {
 3         private static var socket:Socket = null;
 4         private var view:DisplayObject;
 5         
 6         public function SocketController(host:String = null, port:int = 0, view:DisplayObject = null)
 7         {
 8             if(!socket)
 9                 socket = new Socket();
10             this.view = view;
11             configureListener();
12             if(host && port != 0)
13             {
14                 socket.connect(host,port);
15             }
16         }
17         
18         
19         private function configureListener():void
20         {
21             socket.addEventListener(Event.CONNECT, handleConnect);
22             socket.addEventListener(Event.CLOSE, handleClose);
23             socket.addEventListener(ProgressEvent.SOCKET_DATA, handleRecieve);
24         }
25         
26         private function handleConnect(e:Event):void
27         {
28             socket.writeUTFBytes(RequestState.REQUESTLIST);
29             socket.flush();
30         }
31         
32         private function handleClose(e:Event):void
33         {
34             if(socket.connected)
35                 socket.close();
36             socket = null;
37         }
38         
39         private function handleRecieve(e:Event):void
40         {
41             var obj:Object = socket.readObject();
42             if(socket.connected)
43                 socket.close();
44             var musicList:ArrayCollection = obj.musicNames;
45             if(view)
46                 view.dispatchEvent(new MusicListEvent(MusicListEvent.LISTRECIEVE,musicList));        
47         }
48         
49     }

Flex的socket都是異步方式來實現的,通過事件來處理,可以看到第21~23行為socket添加了幾個事件監聽,
第一個是建立連接成功的事件監聽,第二個是連接關閉的監聽,第三個是得到服務端返回消息的監聽。 

 

程序還在不斷的完善,本人也在學習當中,如果有興趣的朋友,可以告訴我好的學習資料,或有什么好的建議,也希望告訴我哦.

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM