2017年10月11日 星期三

如何實作 Ionic 3 的 WebRTC

 不少人都在詢問如何實作cordovaWebRTC,目前作者使用Peer.JS 就能完成一連串的影像互傳的功能,此篇文章在『Angular2 簡易版的webRTC』已簡單介紹過了,目前要更進一步實作出Cordova 在手機上與電腦也能互傳影像。

一、介紹FireBase
支援real-time的一種雲端服務,更重要的一點是完全免費的喔!!! 簡單的key-valueJSON格式,支援資料庫的匯入和匯出,順帶一提免費版的有些容量是有限制的。

為何需要雲端資料庫FireBase儲存資料,因為Peer.js 會產生一組perid讓對方可以透過perid 互傳影像,所以儲存登入者名稱和peerid…

二、登入FireBase 開啟功能


Google 登入à go to console à新增專案




















點選overview旁邊的設定à專案設定



選擇 firebase 加入網路應用程式

複製以下的設定檔
















一、建立ionic 專案

ionic start myApp tabs


$ ionic cordova plugin add cordova-plugin-firebase
$ npm install --save @ionic-native/firebase 
$ npm install angularfire2 firebase --save

Providers/databaseservice/user.ts (實作資料層)

import { Injectable } from '@angular/core';
import {AngularFireDatabase,FirebaseListObservable } from 'angularfire2/database';

@Injectable()
export class UserService {
   constructor(private _af: AngularFireDatabase) {
   }

   public listUsers(): FirebaseListObservable<any[]>{
      return this._af.list('users');
   }

   public addUser(userName,peerID){
        this.listUsers().push({ userName: userName, peerID: peerID});
   }
  
   public removeUser(userName){
       let removeItem = this._af.list('users', {
           query: {
               orderByChild: "userName",
               equalTo: userName
           }
       });

    // Query the list for the purposes of this example:
       removeItem.subscribe((items) => {
           // Remove the matching item:
           if (items.length) {
               removeItem.remove(items[0].$key)
                   .then(() => console.log('removed ' + items[0].$key))
                   .catch((error) => console.log(error));
           }
       });
   }
}

修改app.module.ts
import { AngularFireModule } from 'angularfire2';
import {AngularFireDatabaseModule} from 'angularfire2/database';
import { UserService } from "../providers/databaseservice/users";

// AF2 Settings
export const firebaseConfig = {
  apiKey: "xxxxxxxxxx",
  authDomain: "xxxxxxxxx.firebaseapp.com",
  databaseURL: "https:// xxxxxxxxx.firebaseio.com",
  storageBucket: "xxxxxxxxx.appspot.com",
  messagingSenderId: "xxxxxxxxx"
};


注入module
imports: [
    BrowserModule,
    IonicModule.forRoot(MyApp),
    AngularFireModule.initializeApp(firebaseConfig),
    AngularFireDatabaseModule
  ],

注入prvider
  providers: [
    StatusBar,
    SplashScreen,
    UserService,
    {provide: ErrorHandler, useClass: IonicErrorHandler},
  ]

ionic g page login  建立登入頁

pages/login/login.html
<ion-header>
  <ion-navbar color="primary">
    <ion-title  align-title="center">登錄</ion-title>
  </ion-navbar>
</ion-header>
<ion-content padding>
  <ion-item>
    <ion-label fixed>名稱</ion-label>
    <ion-input type="text" [(ngModel)]="name"></ion-input>
  </ion-item>

  <ion-item>
    <ion-label fixed>密碼</ion-label>
    <ion-input type="text" [(ngModel)]="password"></ion-input>
  </ion-item>
  <p>
   <button ion-button full round (click)="login(name)">登錄</button>
</ion-content>

pages/login/login.ts
@Component({
  selector: 'page-login',
  templateUrl: 'login.html',
})
export class LoginPage {

  constructor(public navCtrl: NavController, public navParams: NavParams) {
  }

  login(name) {
     登入頁面並傳入參數
     this.navCtrl.push(TabsPage, {
      userName: name
    });
  }
}

pages/tabs/tabs.ts
import { Component } from '@angular/core';
import { HomePage } from '../home/home';
import { NavController,NavParams } from 'ionic-angular';

@Component({
  templateUrl: 'tabs.html'
})
export class TabsPage {
  userName; 宣告參數
  tab1Root = HomePage;

  constructor(params: NavParams) {
     this.userName = params.get("userName"); 取得參數
  }
}  

pages/home/home.html
<ion-header>
  <ion-navbar>
    <ion-title>{{viewUserName}} 歡迎</ion-title>
  </ion-navbar>
</ion-header>

<ion-content padding>
    <h1>{{mypeerid}}</h1>
      <ion-card>
        <ion-card-content>
            <video #myvideo></video>
        </ion-card-content>
    </ion-card>
    <ion-item *ngFor="let user of users | async"
        item="item" (click)="startCall(user.userName,user.peerID)">
        {{user.userName}} - {{user.$key}}</ion-item>

     <button ion-button full (click)="videoconnect()">開始通話</button>
     <button ion-button full (click)="stopCall(viewUserName)">結束通話</button>
</ion-content>

pages/home/home.ts
import { Component, ViewChild, OnInit } from '@angular/core';
import { NavController, NavParams } from 'ionic-angular';
import { FirebaseListObservable } from 'angularfire2/database'; 注入fireBase的相關模組
import { UserService } from "../../providers/databaseservice/users";

declare var Peer: any;   宣告peer的物件
@Component({
  selector: 'page-home',
  templateUrl: 'home.html'
})

export class HomePage implements OnInit {
  title = 'app';
  mypeerid: any;
  peer;
  anotherid;
  key;
  @ViewChild('myvideo') myVideo: any取得myVideo的元素
  users: FirebaseListObservable<any[]> fireBase的陣列集合
  viewUserName:any;
  _streamCopy;
  constructor(public navCtrl: NavController, public navParams: NavParams, public userService: UserService) {
    this.users = userService.listUsers(); 取得資料users的集合
    this.viewUserName=navParams.data;
  }

  ngOnInit() {
    let video = this.myVideo.nativeElement;
    this.peer = new Peer({ host: 'peerjs-server.herokuapp.com', secure: true, port: 443 })
    this.mypeerid = this.peer.id;
    setTimeout(() => {
      this.mypeerid = this.peer.id;
       this.userService.addUser(this.viewUserName,this.peer.id );
    }, 1000);
     
    this.peer.on('connection', function (conn) {
      conn.on('data', function (data) {
        console.log(data);
      });
    });

    var n = <any>navigator;
    n.getUserMedia = (n.getUserMedia || n.webkitGetUserMedia || n.mozGetUserMedia || n.msGetUserMedia);
    this.peer.on('call', function (call) {
      n.getUserMedia({ video: true, audio: false }, function (stream) {
        call.answer(stream);
        call.on('stream', function (remotestream) {
          video.src = URL.createObjectURL(remotestream);
          video.play();
        })
      }, function (err) {
        console.log('Failed to get stream', err);
      })
    })
  }

  startCall(userName,anotherID) {
    this.anotherid = anotherID;
    var conn = this.peer.connect(anotherID);
    conn.on('open', function () {
      conn.send('Message from that id');
    });
  }

   this.peer.on('disconnected', function () {
console.log("disconnected");
});

  videoconnect(){
    let video = this.myVideo.nativeElement;
    var localvar = this.peer;
    var fname = this.anotherid;
    var n = <any>navigator;
    console.log("another peerID:"+this.anotherid);
    n.getUserMedia = ( n.getUserMedia || n.webkitGetUserMedia || n.mozGetUserMedia  || n.msGetUserMedia );
   
    n.getUserMedia({
      video:  { width: 700, height: 720 } , audio:false
    }, function(stream) {
      var call = localvar.call(fname, stream);
      call.on('stream', function(remotestream) {
        video.src = URL.createObjectURL(remotestream);
        video.play();
      })
    }, function(err){
      alert(err.name);
      console.log('Failed to get stream', err);
    })
  }

  stopCall(userName) {
    this.userService.removeUser(userName);
    this.peer.destroy();
  }
}

Ionic cordova build android

設定Android的權限

Platforms/android/ AndroidManifest.xml
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.CAMERA" />
    <uses-feature android:name="android.hardware.camera" />
    <uses-feature android:name="android.hardware.camera.autofocus" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />


圖片示範