Skype Web SDK の Audio / Video プログラミング


Skype for Business API 開発

こんにちは。

Skype Web SDK を使った Audio / Video の処理について、実際のプログラミングと内部動作も含め紹介します。

補足 : ここでは Skype for Business Online (Office 365) を使用しますが、Skype for Business Server (On-Prem) の場合の事前セットアップやプログラム コードは「Skype Web SDK の Audio / Video プログラミング (Spring 2015 版)」 (古い情報ですが、設定方法やコードはほぼ同じです) を参照してください。

Skype と ORTC API の経緯 (技術背景)

Web Browser における Real-time Communication の手段として、現在、WebRTC (WebRTC 1.0) が積極的に利用されていますが、Skype、Hangouts (ハングアウト) などの具体的な製品を持つ IT ベンダー (主に Microsoft、Google など) を中心に、さらに柔軟性を高めた ORTC (Object Real-time Communication) API の仕様化の作業が W3C で進んでいます。(また、ORTC Community Group は、WebRTC の次期版へ向けた仕様策定にも大きく関わっています。)

特に Microsoft (米国) はこの活動に熱心で、昨年の 2015 年 10 月に、Web ブラウザーとしては初めて、Edge で ORTC API を実装しました。

Windows Blog : ORTC API is now available in Microsoft Edge
https://blogs.windows.com/msedgedev/2015/09/18/ortc-api-is-now-available-in-microsoft-edge/

現在、、Edge 上で ORTC の Low Level API (JavaScript API) を試すことができますが、この ORTC の活動には、Edge チームだけなく、Skype チームも深く関わっています。(ここで言う “Skype” は、Skype と Skype for Business (旧 Lync) の双方を含みます。最終的には、これら 2 つの開発 Platform は、同じ方向で進んでいきます。)

(以下、当時の Windows Blog より抜粋)

ORTC Support in Microsoft Edge is the result of a close collaboration between Microsoft’s Operating Systems Group and Skype teams. …
Our goal is to enable developers around the world to build experiences that include the ability to talk to Skype users and other WebRTC compatible communication services.

ご存じの方も多いかと思いますが、Skype for Business (当時は、まだ “Lync”) の Web 標準 API である UCWA が登場してから (2013 年)、割と早い段階で、音声・ビデオの API についても対応を計画していました。しかし、実際にこの API はなかなか出てくることはなく、結局、昨年 (2015年) 春の Preview 版の登場まで、2 年近く待たせたことになります。(Microsoft にしては、あまりに遅い対応でした。) この理由は、ORTC API を見据えた活動を計画していたためです。

Skype Blog : Bringing Interoperable Real-Time Communications to the Web
http://blogs.skype.com/2014/10/27/bringing-interoperable-real-time-communications-to-the-web/

そして遂に、ORTC API の仕様策定 (検討) が進んだ昨年春 (BUILD 2015 のタイミング) に、Skype 会議の Web 参加 (ゲスト参加) で使用されていた Skype for Business Web App Plug-in を使った音声・動画の API (Preview) が公開されました。
もちろん、当時は、Edge も含め、ORTC API を実装した Web Browser は存在していません。このため、この API は Plug-in に完全に依存していましたが (内部では、WebRTC も ORTC もまったく使っていません)、このリリースの意味は、「今後、同じような API が出てくる」ということを示した Developer Preview を提供して「使い方」を示すと共に、今後のマイルストーンとして ORTC API への対応を表明する点にありました。(後述する本投稿のサンプル コードを見るとわかりますが、プログラミング内容は、下記の当時のサンプル コードとほとんど変わりません。)

Skype Web SDK – Spring 2015
https://blogs.msdn.microsoft.com/tsmatsuz/2015/06/02/skype-web-sdk-audio-video/

さらに、(Skype for Business ではない) Skype 側も、直後の 2015 年 6 月に、軽量 Plug-in を使った Skype for Web の Preview を提供します。この Preview も同様に、「今後、このような使い勝手になる」という意思表示であり、今後の ORTC API 対応を見据えた評価リリースです。(ただし、この Plug-in は、上記の Skype for Business Web App Plug-in と異なり、必要最小限の処理のみをおこなう小さな Plug-in でした。)

Skype Blog : Skype for Web (Beta) Now Available Worldwide
https://blogs.skype.com/2015/06/05/skype-for-web-beta-is-now-available-to-everyone-in-the-us-and-uk/

そして、上述の通り、2015 年 10 月に、Edge ブラウザーに ORTC API が実装されました。これにあわせて、Edge 上でのみ Plug-in なし (Plug-in Free) で動作する Skype for Web の Update も提供されました。(Edge 以外の他ブラウザーでは軽量の Plug-in を必要とします。)

Skype Blog : Skype for Web and Skype for Outlook.com (Update)
http://blogs.skype.com/2015/09/18/skype-for-web-and-skype-for-outlook-com-update/

前置きが長くなりましたが、本投稿で紹介する Skype Web SDK は、こうした流れ (経緯) に沿って BUILD 2016  (2016 年春) のタイミングで登場した「Edge 上で Plug-in Free で動作する Skype for Business の API」です。この歴史は、Microsoft 以外の団体も含めた合意に基づく作業でもあり、このように 3 年越でようやく形が見えてきました。
もちろん、この実装も最終的な姿ではありません。(現在 Preview です。) 将来的には、他ブラウザーで対応している WebRTC のカレントへの何らかの対応も見据えているようなので、「まずは Edge でお試しください」ということです。

上述の通り、現 Preview では、この Skype Web SDK の Audio / Video の処理は Chrome、Safari などでは動作しません。(1 エンジニアの予測として、次は Chrome あたりの対応が期待できますが。。。)
このため、現段階 (2016/04) では、必ず Edge を使用して以降の動作を確認してください。(上述の通り、Edge に Plug-in 等の面倒な準備はいっさい不要です。)

補足 : Azure AD アカウント (もしくは、Azure AD と Federation された AD アカウント) で Windows にログインしている場合は注意してください。これについては「Skype Web SDK 紹介 (Skype for Business Online)」を参照してください。

では、そのプログラミングと動きを紹介していきます。

 

Skype Web SDK の Audio プログラミング

まず、画面 (UI) が不要な Audio の処理 (プログラミング) を紹介します。

今回は、「Skype Web SDK 紹介」で使用したサンプルに、下記の通り「6. Start Audio Conversation」というボタンを追加します。

<?php
  ... 途中省略
?>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
  <title></title>
  <meta charset="utf-8" />
</head>
<body>
  <script src="https://swx.cdn.skype.com/shared/v/1.2.15/SkypeBootstrap.min.js"></script>
  <button id="btnInit">1.Initialize</button>
  <button id="btnSignIn">2.Sign In</button>
  <button id="btnGetPerson">3.Get Person</button>
  <button id="btnStartIM">4.Start Conversation</button>
  <button id="btnSendIM">5.Send text message</button>
  <button id="btnStartAudio">6. Start Audio Conversation</button>
  ... 以降省略

</body>
</html>

つぎに、ボタンを押した際の処理を実装します。

Audio / Video の処理というと、Media に関する難解な知識が必要と思われるかもしれませんが、Skype Web SDK では直観的な API を呼び出すだけです。(上述の通り、ORTC API そのものを使った Primitive な処理は Edge 上の JavaScript で実装してください。)

前回の「Skype Web SDK 紹介」では、Chat (Text Messaging) をおこなうサンプル コードとして chatService.start() という関数を使いましたが、Audio Conversation を開始するには、下記太字の通り、audioService.start() を呼ぶだけです。(同様に、Video の場合には videoService.start() です。)
Browser と Skype for Business 間での音声通話やビデオ通話に関わる専門的な処理 (シグナリングや Transport レベルのオブジェクトの初期化など) は、この一行ですべて実施してくれます。

<?php
// (Please change this in-memory session, if production. because not scaling)
session_start();

if(isset($_GET['code'])) {
  //
  // When auth code is supplied, get access token
  //
  $authreq = curl_init();  
  curl_setopt($authreq, CURLOPT_URL,
    'https://login.microsoftonline.com/common/oauth2/token');
  curl_setopt($authreq, CURLOPT_POST, true);  
  curl_setopt($authreq, CURLOPT_HTTPHEADER,
    array('Content-Type: application/x-www-form-urlencoded'));
  // (Please do the admin consent first, if production)
  curl_setopt($authreq, CURLOPT_POSTFIELDS, http_build_query(array(
    'grant_type' => 'authorization_code',
    'code' => $_GET['code'],
    'client_id' => '205de2e9-6fbc-40ec-aed9-9da7b48bee21',
    'client_secret' => 'WaBfOMLgHG...',
    'redirect_uri' => 'https://mydeploy.azurewebsites.net/skype.php',
  )));
  curl_setopt($authreq, CURLOPT_RETURNTRANSFER, true);
  // (Please change the following SSL verify mode, if production)
  curl_setopt($authreq, CURLOPT_SSL_VERIFYPEER, false);
  $authres = json_decode(curl_exec($authreq));
  curl_close($authreq);
  $_SESSION['access_token'] = $authres->access_token;
}

//
// If initial access, redirect page to get auth code
//
if(!isset($_SESSION['access_token']))
  header('Location: https://login.microsoftonline.com/common/oauth2/authorize'
    .'?response_type=code'
    .'&client_id='
    .'205de2e9-6fbc-40ec-aed9-9da7b48bee21'
    .'&resource='
    .urlencode('https://webdir.online.lync.com')
    .'&redirect_uri='
    .urlencode('https://mydeploy.azurewebsites.net/skype.php'));

// (Please check token, if production)
// see https://blogs.msdn.microsoft.com/tsmatsuz/2015/02/17/azure-ad-service-access-token-validation-check/
?>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
  <title></title>
  <meta charset="utf-8" />
</head>
<body>
  <script src="https://swx.cdn.skype.com/shared/v/1.2.15/SkypeBootstrap.min.js"></script>
  <button id="btnInit">1.Initialize</button>
  <button id="btnSignIn">2.Sign In</button>
  <button id="btnGetPerson">3.Get Person</button>
  <button id="btnStartIM">4.Start Conversation</button>
  <button id="btnSendIM">5.Send text message</button>
  <button id="btnStartAudio">6. Start Audio Conversation</button>
  <script>
    (function () {
      var Application;
      var app;
      var person;
      var conver;

      //
      // 1.Initialize Skype Web SDK
      //
      document.getElementById('btnInit').onclick = function () {
        Skype.initialize({
          apiKey: 'a42fcebd-5b43-4b89-a065-74450fb91255',
        }, function (api) {
          Application = api.application;
          app = new Application();
          alert('initialize succeed');
        }, function (err) {
          alert('initialize error: ' + err);
        });
      }

      //
      // 2.SignIn
      //
      document.getElementById('btnSignIn').onclick = function () {
        app.signInManager.signIn({
          "client_id": "205de2e9-6fbc-40ec-aed9-9da7b48bee21",
          "origins": ["https://webdir.online.lync.com/autodiscover/autodiscoverservice.svc/root"],
          "cors": true,
          "version": 'SkypeOnlinePreviewApp/1.0.0',
          "redirect_uri": 'https://mydeploy.azurewebsites.net/skype.php'
        }).then(function () {
          alert('signed in');
        }).then(null, function (err) {
          alert('signin error: ' + err);
        });
      }

      //
      // 3.Get Person to communicate (Show status)
      //
      document.getElementById('btnGetPerson').onclick = function () {
        var personSearchQuery =
          app.personsAndGroupsManager.createPersonSearchQuery();
        personSearchQuery.text('Demo Taro');
        personSearchQuery.limit(50);
        personSearchQuery.getMore(
        ).then(function (results) {
          person = results[0].result;
          person.status.get().then(function (status) {
            alert('Status is: ' + status);
          });
        }).then(null, function (err) {
          alert('Search error: ' + err);
        });
      }

      . . .

      //
      // 6.Start Audio Conversation
      //
      document.getElementById('btnStartAudio').onclick = function () {
        conver = app.conversationsManager.getConversation(person);
        conver.selfParticipant.audio.state.changed(function (state) {
          alert('state:' + state);
        });
        conver.audioService.start();
      }

    }());
  </script>
</body>
</html>

では、動作させてみましょう。

表示される画面の「1.Initialize」、「2.Sign In」、「3.Get Person」、「6. Start Audio Conversation」の順番にボタンを押してみてください。
音声通話・ビデオ通話の開始の際、state は、Disconnected -> Connecting -> Connected と遷移します。(上記の通り、プログラムで state を取得して alert 表示しています。)
また、会話が開始されると、Web Browser (Edge) が、下図の通りマイクの利用許可を求めてきます。許可すると、ブラウザーを通して、相手 (Skype for Business など) との音声通話が可能になります。

Audio / Video に対する簡単な操作 (Operation) も可能です。
例えば、下記は、Audio が UnMute の場合は Mute (音声の送信停止) に設定し、Mute されている場合には UnMute に切り替えます。

<body>
  . . .
  <button id="btnMuteAudio">Mute/Unmute Audio</button>
  . . .

  <script>
    (function () {
      . . .

      document.getElementById('btnMuteAudio').onclick = function () {
        var conv = app.conversationsManager.conversations(0);
        var audio = conv.selfParticipant.audio;
        audio.isMuted.set(!audio.isMuted());
      }
      . . .

    }());
  </script>
</body>

 

Skype Web SDK の Video プログラミング

今度は、Video 通話をしてみましょう。

Audio と違い、今度は画面 (通話相手の表示など) の実装も必要です。
そこで、今回は、Video 通話を開始するボタンと、ビデオ通話を表示する領域 (div) を下記太字の通り追加します。

<?php
  ... 途中省略

?>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
  <title></title>
  <meta charset="utf-8" />
</head>
<body>
  <script src="https://swx.cdn.skype.com/shared/v/1.2.15/SkypeBootstrap.min.js"></script>
  <button id="btnInit">1.Initialize</button>
  <button id="btnSignIn">2.Sign In</button>
  <button id="btnGetPerson">3.Get Person</button>
  <button id="btnStartIM">4.Start Conversation</button>
  <button id="btnSendIM">5.Send text message</button>
  <button id="btnStartAudio">6. Start Audio Conversation</button>
  <button id="btnStartVideo">7. Start Video Conversation</button>
  <p>
    <div id="render-window" style="position:absolute;top:170px;left:0px;width:200px;height:200px;">
    </div>
  </p>
  <script>
    ... 途中省略

  </script>
</body>
</html>

Audio 通話同様、videoService.start() で Video 通話を開始できます。
Video の映像の受信は、下記太字の通り、DOM の Element (上記の div 要素) に対して Video の stream を設定することで、そこに映像をリアルタイムに表示します。(これも、簡単で直観的なプログラミングですね。)

<?php
  ... 途中省略
?>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
  <title></title>
  <meta charset="utf-8" />
</head>
<body>
  <script src="https://swx.cdn.skype.com/shared/v/1.2.15/SkypeBootstrap.min.js"></script>
  <button id="btnInit">1.Initialize</button>
  <button id="btnSignIn">2.Sign In</button>
  <button id="btnGetPerson">3.Get Person</button>
  <button id="btnStartIM">4.Start Conversation</button>
  <button id="btnSendIM">5.Send text message</button>
  <button id="btnStartAudio">6. Start Audio Conversation</button>
  <button id="btnStartVideo">7. Start Video Conversation</button>
  <p>
    <div id="render-window" style="position:absolute;top:170px;left:0px;width:200px;height:200px;">
    </div>
  </p>
  <script>
    (function () {
      var Application;
      var app;
      var person;
      var conver;

      //
      // 1.Initialize Skype Web SDK
      //
      document.getElementById('btnInit').onclick = function () {
        Skype.initialize({
          apiKey: 'a42fcebd-5b43-4b89-a065-74450fb91255',
        }, function (api) {
          Application = api.application;
          app = new Application();
          alert('initialize succeed');
        }, function (err) {
          alert('initialize error: ' + err);
        });
      }

      //
      // 2.SignIn
      //
      document.getElementById('btnSignIn').onclick = function () {
        app.signInManager.signIn({
          "client_id": "205de2e9-6fbc-40ec-aed9-9da7b48bee21",
          "origins": ["https://webdir.online.lync.com/autodiscover/autodiscoverservice.svc/root"],
          "cors": true,
          "version": 'SkypeOnlinePreviewApp/1.0.0',
          "redirect_uri": 'https://mydeploy.azurewebsites.net/skype.php'
        }).then(function () {
          alert('signed in');
        }).then(null, function (err) {
          alert('signin error: ' + err);
        });
      }

      //
      // 3.Get Person to communicate (Show status)
      //
      document.getElementById('btnGetPerson').onclick = function () {
        var personSearchQuery =
          app.personsAndGroupsManager.createPersonSearchQuery();
        personSearchQuery.text('Demo Taro');
        personSearchQuery.limit(50);
        personSearchQuery.getMore(
        ).then(function (results) {
          person = results[0].result;
          person.status.get().then(function (status) {
            alert('Status is: ' + status);
          });
        }).then(null, function (err) {
          alert('Search error: ' + err);
        });
      }

      ... 途中省略

      //
      // 7.Start Video Conversation
      //
      document.getElementById('btnStartVideo').onclick = function () {
        conver = app.conversationsManager.getConversation(person);
        conver.participants.added(function (p) {
          p.video.channels(0).stream.source.sink
            .container(document.getElementById("render-window"));
        });
        conver.selfParticipant.video.state.changed(function (state) {
          alert('state:' + state);
        });
        conver.videoService.start();        
      }

    }());
  </script>
</body>
</html>

では、実行してみましょう。
上図の画面の「1.Initialize」、「2.Sign In」、「3.Get Person」、「7. Start Video Conversation」の順番にボタンを押してください。

会話の開始時に、今度は、Web Browser がマイクとカメラへのアクセス許可を求めてきます。

デバイス アクセスを許可して音声・ビデオ通話が開始されると、前述の div タグの要素に相手の映像 (stream) がリアルタイムに表示されます。

Audio 同様、Video の場合も、さまざまな Operation が可能です。(詳細の解説は省略します。)

 

なお、今回使った最終的なサンプル コードは下記の通りです。
client id, client secret, redirect uri などは適宜変更してお試しください。また、「Skype Web SDK 紹介」で記載した注意事項もお忘れなく。

<?php
// (Please change this in-memory session, if production. because not scaling)
session_start();

if(isset($_GET['code'])) {
  //
  // When auth code is supplied, get access token
  //
  $authreq = curl_init();  
  curl_setopt($authreq, CURLOPT_URL,
    'https://login.microsoftonline.com/common/oauth2/token');
  curl_setopt($authreq, CURLOPT_POST, true);  
  curl_setopt($authreq, CURLOPT_HTTPHEADER,
    array('Content-Type: application/x-www-form-urlencoded'));
  // (Please do the admin consent first, if production)
  curl_setopt($authreq, CURLOPT_POSTFIELDS, http_build_query(array(
    'grant_type' => 'authorization_code',
    'code' => $_GET['code'],
    'client_id' => '205de2e9-6fbc-40ec-aed9-9da7b48bee21',
    'client_secret' => 'WaBfOMLgHG...',
    'redirect_uri' => 'https://mydeploy.azurewebsites.net/skype.php',
  )));
  curl_setopt($authreq, CURLOPT_RETURNTRANSFER, true);
  // (Please change the following SSL verify mode, if production)
  curl_setopt($authreq, CURLOPT_SSL_VERIFYPEER, false);
  $authres = json_decode(curl_exec($authreq));
  curl_close($authreq);
  $_SESSION['access_token'] = $authres->access_token;
}

//
// If initial access, redirect page to get auth code
//
if(!isset($_SESSION['access_token']))
  header('Location: https://login.microsoftonline.com/common/oauth2/authorize'
    .'?response_type=code'
    .'&client_id='
    .'205de2e9-6fbc-40ec-aed9-9da7b48bee21'
    .'&resource='
    .urlencode('https://webdir.online.lync.com')
    .'&redirect_uri='
    .urlencode('https://mydeploy.azurewebsites.net/skype.php'));

// (Please check token, if production)
// see https://blogs.msdn.microsoft.com/tsmatsuz/2015/02/17/azure-ad-service-access-token-validation-check/
?>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
  <title></title>
  <meta charset="utf-8" />
</head>
<body>
  <script src="https://swx.cdn.skype.com/shared/v/1.2.15/SkypeBootstrap.min.js"></script>
  <button id="btnInit">1.Initialize</button>
  <button id="btnSignIn">2.Sign In</button>
  <button id="btnGetPerson">3.Get Person</button>
  <button id="btnStartIM">4.Start Conversation</button>
  <button id="btnSendIM">5.Send text message</button>
  <button id="btnStartAudio">6. Start Audio Conversation</button>
  <button id="btnStartVideo">7. Start Video Conversation</button>
  <p>
    <div id="render-window" style="position:absolute;top:170px;left:0px;width:200px;height:200px;">
    </div>
  </p>
  <script>
    (function () {
      var Application;
      var app;
      var person;
      var conver;

      //
      // 1.Initialize Skype Web SDK
      //
      document.getElementById('btnInit').onclick = function () {
        Skype.initialize({
          apiKey: 'a42fcebd-5b43-4b89-a065-74450fb91255',
        }, function (api) {
          Application = api.application;
          app = new Application();
          alert('initialize succeed');
        }, function (err) {
          alert('initialize error: ' + err);
        });
      }

      //
      // 2.SignIn
      //
      document.getElementById('btnSignIn').onclick = function () {
        app.signInManager.signIn({
          "client_id": "205de2e9-6fbc-40ec-aed9-9da7b48bee21",
          "origins": ["https://webdir.online.lync.com/autodiscover/autodiscoverservice.svc/root"],
          "cors": true,
          "version": 'SkypeOnlinePreviewApp/1.0.0',
          "redirect_uri": 'https://mydeploy.azurewebsites.net/skype.php'
        }).then(function () {
          alert('signed in');
        }).then(null, function (err) {
          alert('signin error: ' + err);
        });
      }

      //
      // 3.Get Person to communicate (Show status)
      //
      document.getElementById('btnGetPerson').onclick = function () {
        var personSearchQuery =
          app.personsAndGroupsManager.createPersonSearchQuery();
        personSearchQuery.text('Demo Taro');
        personSearchQuery.limit(50);
        personSearchQuery.getMore(
        ).then(function (results) {
          person = results[0].result;
          person.status.get().then(function (status) {
            alert('Status is: ' + status);
          });
        }).then(null, function (err) {
          alert('Search error: ' + err);
        });
      }

      //
      // 4.Start conversation
      //
      document.getElementById('btnStartIM').onclick = function () {
        conver = app.conversationsManager.getConversation(person);
        conver.selfParticipant.chat.state.changed(function (state) {
          if (state == 'Connected') {
            alert('connected');
          }
        });
        conver.chatService.start();

        // (receiving message !)
        conver.historyService.activityItems.added(function (item) {
          if (item.type() == 'TextMessage') {
            if (item.direction() == 'Incoming') {
              alert('received message: ' + item.text());
            }
          };
        });

        alert('set-up finished');  
      }

      //
      // 5.Send text message
      //
      document.getElementById('btnSendIM').onclick = function () {
        conver.chatService.sendMessage('hello world !');
      }

      //
      // 6.Start Audio Conversation
      //
      document.getElementById('btnStartAudio').onclick = function () {
        conver = app.conversationsManager.getConversation(person);
        conver.selfParticipant.audio.state.changed(function (state) {
          alert('state:' + state);
        });
        conver.audioService.start();
      }

      //
      // 7.Start Video Conversation
      //
      document.getElementById('btnStartVideo').onclick = function () {
        conver = app.conversationsManager.getConversation(person);
        conver.participants.added(function (p) {
          p.video.channels(0).stream.source.sink
            .container(document.getElementById("render-window"));
        });
        conver.selfParticipant.video.state.changed(function (state) {
          alert('state:' + state);
        });
        conver.videoService.start();        
      }

    }());
  </script>
</body>
</html>

 

Comments (0)

Skip to main content