智能电视TV开发---客户端和服务器通信
在做智能电视应用的时候,最头疼的就是焦点问题,特别是对于个人开发者,没有设备这是最最头疼的事情了,在没有设备的情况下,怎么实现智能电视应用呢,接下来我是用TV程序来做演示的,所以接下来的所有操作是在有网络的情况下,TV链接到一个路由器上面,做过开发的人都知道Socket编程分为两种一个是可靠传输的TCP,另一个是不可靠传输的UDP,TCP需要知道对方的IP才能实现,UDP虽然不可靠,但是它可以实现广播来进行通信,从而得知对方的IP地址,然后就可以TCP通信了,对于智能电视的TV开发,如果你没有设备,则可以利用UDP的这个特性来实现手机操控电视,建立通信协议,然后TV端Server接收广播,手机端作为Client发送广播,所有的操作放在手机端来实现,TV只接收并处理相应的命令。
一、UDP实现
首先就是实现UDP的广播通信,下面就是UDP的Server和Client代码:
Server:为了实现能够长时间的接收客户端的信息,所以要把Server端放在线程里面如下:
/** * 实现后台监听广播 * @author jwzhangjie */ private class UdpServerRunable implements Runnable { @Override public void run() { byte[] data = new byte[256]; DatagramPacket udpPacket = new DatagramPacket(data, 256); try { udpSocket = new DatagramSocket(43708); } catch (Exception e) { e.printStackTrace(); } while (!isStop) { try { udpSocket.receive(udpPacket); if (udpPacket.getLength() != 0) { Url = new String(data, 0, udpPacket.getLength()); Log.e(TAG, Url); if (onUdpServerCallBackListener != null) { onUdpServerCallBackListener.onPlayUrl(Url); } } } catch (Exception e) { } } } };
为了测试方便我先阶段是Client放在PC端来实现的,为了实现循环测试,我也是把客户端放在一个线程里面,代码如下:
public class Test_UDP_Client{ public static void main(String[] args){ new Thread(new Runnable() { int i = 0; private byte[] buffer = new byte[40]; @Override public void run() { DatagramPacket dataPacket = null; DatagramSocket udpSocket = null; List<String> listData = new ArrayList<String>(); listData.add("http://live.gslb.letv.com/gslb?stream_id=hunan&tag=live&ext=m3u8&sign=live_tv"); listData.add("http://play.api.pptv.com/web-m3u8-300161.m3u8?type=m3u8.web.pad"); try { udpSocket = new DatagramSocket(43708); dataPacket = new DatagramPacket(buffer, 40); dataPacket.setPort(43708); InetAddress broadcastAddr; broadcastAddr = InetAddress.getByName("255.255.255.255"); dataPacket.setAddress(broadcastAddr); } catch (Exception e) { } while (i < 30) { i++; try { byte[] data = (listData.get(i%2)).getBytes(); dataPacket.setData( data ); dataPacket.setLength( data.length ); udpSocket.send(dataPacket); Thread.sleep(20000); } catch (Exception e) { e.printStackTrace(); } } udpSocket.close(); } }).start(); } }
二、Service启动Server的线程
线程是不可控的,如果Activity突然的挂掉,那么这个线程还是在后台运行的,所以我们要把Server放在Service里面,通过Service来启动服务端,代码如下:
package com.jwzhangjie.smart_tv.server; import java.net.DatagramPacket; import java.net.DatagramSocket; import com.jwzhangjie.smart_tv.interfaces.UdpServerCallBackListener; import android.app.Service; import android.content.Intent; import android.os.Binder; import android.os.IBinder; import android.util.Log; public class CommandServer extends Service{ private static String TAG = CommandServer.class.getName(); public static boolean isStop = false; private DatagramSocket udpSocket = null; private Thread udpServerThread; private String Url; /** * 设置视频连接的回调接口 */ private UdpServerCallBackListener onUdpServerCallBackListener; @Override public IBinder onBind(Intent intent) { Log.e(TAG, "onBind"); startListener(); return new LocalBinder(); } /** * 注册回调接口的方法,供外部调用 * @param onUdpServerCallBackListener */ public void setOnUdpServerCallBackListener(UdpServerCallBackListener onUdpServerCallBackListener){ this.onUdpServerCallBackListener = onUdpServerCallBackListener; } @Override public void onCreate() { Log.e(TAG, "onCreate"); super.onCreate(); } @Override public int onStartCommand(Intent intent, int flags, int startId) { Log.e(TAG, "onStartCommand"); startListener(); return super.onStartCommand(intent, flags, startId); } @Override public void onDestroy() { Log.e(TAG, "onDestroy"); isStop = true; udpSocket.disconnect(); udpSocket.close(); udpServerThread.interrupt(); udpServerThread = null; super.onDestroy(); } @Override public boolean onUnbind(Intent intent) { Log.e(TAG, "onUnbind"); return super.onUnbind(intent); } /** * 开始监听广播 */ private void startListener(){ if (udpServerThread == null) { Log.e(TAG, "run"); udpServerThread = new Thread(new UdpServerRunable()); udpServerThread.start(); } } /** * 实现后台监听广播 * @author pig_video */ private class UdpServerRunable implements Runnable { @Override public void run() { byte[] data = new byte[256]; DatagramPacket udpPacket = new DatagramPacket(data, 256); try { udpSocket = new DatagramSocket(43708); } catch (Exception e) { e.printStackTrace(); } while (!isStop) { try { udpSocket.receive(udpPacket); if (udpPacket.getLength() != 0) { Url = new String(data, 0, udpPacket.getLength()); Log.e(TAG, Url); if (onUdpServerCallBackListener != null) { onUdpServerCallBackListener.onPlayUrl(Url); } } } catch (Exception e) { } } } }; //定义内部类继承Binder public class LocalBinder extends Binder{ //返回本地服务 public CommandServer getService(){ return CommandServer.this; } } }
3.Activity与Service的绑定
我这里启动Service的方式是通过Activity的onBind来启动的,当Activity关闭的时候,也将Service关闭同时关闭Server的线程,当然常驻后台也行,不过用户可能不太喜欢,毕竟需要资源,播放器我选用的是免费的Vitamio主要是他们把上层应用的代码也提供出来了,非常省事。
package com.jwzhangjie.smart_tv.player; import io.vov.vitamio.LibsChecker; import io.vov.vitamio.MediaPlayer; import io.vov.vitamio.MediaPlayer.OnBufferingUpdateListener; import io.vov.vitamio.MediaPlayer.OnErrorListener; import io.vov.vitamio.MediaPlayer.OnInfoListener; import io.vov.vitamio.MediaPlayer.OnTimedTextListener; import io.vov.vitamio.widget.MediaController; import io.vov.vitamio.widget.VideoView; import com.jwzhangjie.smart_tv.R; import com.jwzhangjie.smart_tv.dialog.JWDialogLoading; import com.jwzhangjie.smart_tv.interfaces.UdpServerCallBackListener; import com.jwzhangjie.smart_tv.server.CommandServer; import android.os.Bundle; import android.os.IBinder; import android.util.Log; import android.view.View; import android.widget.TextView; import android.widget.Toast; import android.app.Activity; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; public class SmartTV_Server extends Activity implements OnInfoListener, OnBufferingUpdateListener{ private static final String TAG = SmartTV_Server.class.getName(); private String path = "http://live.gslb.letv.com/gslb?stream_id=guangdong&tag=live&ext=m3u8"; private String subtitle_path = ""; private VideoView mVideoView; private TextView mSubtitleView; private long mPosition = 0; private int mVideoLayout = 0; private JWDialogLoading mDialogLoading; private CommandServer pigBackServices; private boolean isFirst = true; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (!LibsChecker.checkVitamioLibs(this)) return; setContentView(R.layout.subtitle2); mDialogLoading = new JWDialogLoading(this, R.style.dialog); //绑定后台接收视频连接的Service Intent intent = new Intent(SmartTV_Server.this, CommandServer.class); bindService(intent, conn, Context.BIND_AUTO_CREATE); mVideoView = (VideoView) findViewById(R.id.surface_view); mSubtitleView = (TextView) findViewById(R.id.subtitle_view); if (path == "") { // Tell the user to provide a media file URL/path. Toast.makeText(SmartTV_Server.this, "Please select video, and set path" + " variable to your media file URL/path", Toast.LENGTH_LONG).show(); return; } else { /* * Alternatively,for streaming media you can use * mVideoView.setVideoURI(Uri.parse(URLstring)); */ isFirst = false; mVideoView.setVideoPath(path); mVideoView.setMediaController(new MediaController(this)); mVideoView.requestFocus(); mVideoView.setOnErrorListener(new OnErrorListener() { @Override public boolean onError(MediaPlayer mp, int what, int extra) { return true; } }); mVideoView.setOnInfoListener(this); mVideoView.setOnBufferingUpdateListener(this); mVideoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { @Override public void onPrepared(MediaPlayer mediaPlayer) { // optional need Vitamio 4.0 mediaPlayer.setPlaybackSpeed(1.0f); mVideoView.addTimedTextSource(subtitle_path); mVideoView.setTimedTextShown(true); } }); mVideoView.setOnTimedTextListener(new OnTimedTextListener() { @Override public void onTimedText(String text) { mSubtitleView.setText(text); } @Override public void onTimedTextUpdate(byte[] pixels, int width, int height) { } }); } } @Override protected void onPause() { mPosition = mVideoView.getCurrentPosition(); mVideoView.stopPlayback(); super.onPause(); } @Override protected void onResume() { if (mPosition > 0) { mVideoView.seekTo(mPosition); mPosition = 0; } super.onResume(); mVideoView.start(); } public void changeLayout(View view) { mVideoLayout++; if (mVideoLayout == 4) { mVideoLayout = 0; } switch (mVideoLayout) { case 0: mVideoLayout = VideoView.VIDEO_LAYOUT_ORIGIN; view.setBackgroundResource(R.drawable.mediacontroller_sreen_size_100); break; case 1: mVideoLayout = VideoView.VIDEO_LAYOUT_SCALE; view.setBackgroundResource(R.drawable.mediacontroller_screen_fit); break; case 2: mVideoLayout = VideoView.VIDEO_LAYOUT_STRETCH; view.setBackgroundResource(R.drawable.mediacontroller_screen_size); break; case 3: mVideoLayout = VideoView.VIDEO_LAYOUT_ZOOM; view.setBackgroundResource(R.drawable.mediacontroller_sreen_size_crop); break; } mVideoView.setVideoLayout(mVideoLayout, 0); } ServiceConnection conn = new ServiceConnection() { @Override public void onServiceDisconnected(ComponentName name) { pigBackServices = null; } @Override public void onServiceConnected(ComponentName name, IBinder service) { pigBackServices = ((CommandServer.LocalBinder)service).getService(); pigBackServices.setOnUdpServerCallBackListener(new UdpServerCallBackListener() { @Override public void onPlayUrl(String url) { path = url; if (isFirst) { isFirst = false; mVideoView.setVideoPath(url); mVideoView.setMediaController(new MediaController(SmartTV_Server.this)); mVideoView.requestFocus(); mVideoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { @Override public void onPrepared(MediaPlayer mediaPlayer) { // optional need Vitamio 4.0 mediaPlayer.setPlaybackSpeed(1.0f); mVideoView.addTimedTextSource(subtitle_path); mVideoView.setTimedTextShown(true); } }); mVideoView.setOnTimedTextListener(new OnTimedTextListener() { @Override public void onTimedText(String text) { mSubtitleView.setText(text); } @Override public void onTimedTextUpdate(byte[] pixels, int width, int height) { } }); }else{ if (mVideoView.isPlaying()) { mVideoView.stopPlayback(); } mVideoView.setVideoPath(url); } Log.e(TAG, url); } }); } }; @Override protected void onDestroy() { if (pigBackServices != null) { unbindService(conn); } super.onDestroy(); } @Override public void onBufferingUpdate(MediaPlayer mp, int percent) { mDialogLoading.setProgreess(percent); } private boolean isStart; @Override public boolean onInfo(MediaPlayer mp, int what, int extra) { switch (what) { case MediaPlayer.MEDIA_INFO_BUFFERING_START: if (mVideoView.isPlaying()) { mVideoView.pause(); isStart = true; if (!mDialogLoading.isShowing()) { mDialogLoading.show(); } } break; case MediaPlayer.MEDIA_INFO_BUFFERING_END: if (isStart) { mVideoView.start(); if (mDialogLoading.isShowing()) { mDialogLoading.dismiss(); } } break; case MediaPlayer.MEDIA_INFO_DOWNLOAD_RATE_CHANGED: mDialogLoading.setValue("" + extra + "kb/s" + " "); break; } return true; } }
4.加载进度
可能你在上面的播放器代码里面会看到JWDialogLoading,这个是一个网上的环形进度框,能够提示视频的记载进度和下载速度,不过你要覆写Vitamio里面的OnInfoListener, OnBufferingUpdateListener这两个接口才行。
package com.jwzhangjie.smart_tv.dialog; import android.app.Activity; import android.app.Dialog; import android.os.Bundle; import android.view.Display; import android.view.Window; import android.view.WindowManager.LayoutParams; import android.widget.LinearLayout; public class JWDialogLoading extends Dialog{ private RadialProgressWidget mProgressWidget; private Activity context; public JWDialogLoading(Activity context) { super(context); this.context = context; mProgressWidget = new RadialProgressWidget(context); } public JWDialogLoading(Activity context, int style) { super(context, style); this.context = context; mProgressWidget = new RadialProgressWidget(context); } @SuppressWarnings("deprecation") @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); LinearLayout.LayoutParams param = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT); setContentView(mProgressWidget, param); mProgressWidget.setSecondaryText("Loading..."); mProgressWidget.setTouchEnabled(false); Window window = getWindow(); LayoutParams params = window.getAttributes(); Display display = context.getWindowManager().getDefaultDisplay(); params.height = (int)(display.getWidth()*0.3); params.width = (int)(display.getWidth()*0.3); params.alpha = 1.0f; window.setAttributes(params); } public void setProgreess(int value){ mProgressWidget.setCurrentValue(value); mProgressWidget.invalidate(); if (value == 100) { dismiss(); } } public void setValue(String content){ mProgressWidget.setSecondaryText("Loading... "+content); } }
5.效果图如下: