WebRTC recording via Kurento media server

Official guide

I encountered some errors while following the Official guide.
Lucky, I fixed all the errors and make it work finally.
Notes my workaround here, hope it work for you if you have the same problem.

gpg: keyserver receive failed: keyserver error

local install guide

solution from stackoverflow

sudo apt-key adv --keyserver hkp://keys.gnupg.net:80 --recv-keys 5AFA7A83

WebSocket connection to 'wss://<local_ip>:8433/kurento' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED

cause

the application server is started with https if you follow the guide:

http-server -p 8443 -S -C keys/server.crt -K keys/server.key 

but kurento media server start without ssl by default config.
You can get from the config and the default port is 8888.
/etc/kurento/kurento.conf.json

solution

modify kurento media server config to enable ssl and use the same cert that used for your application server. it is a little complex.
but we have a easy solution, just start your application without ssl and test with firefox(if use chrome, you will got a dom exception, then you have to enable ssl for kms).

modify js/index.js
ws_uri: 'ws://' + location.hostname + ':8888/kurento'

start application server with
http-server -p 8443

record callback not triggered

if you use kurento-hello-world-recorder-generator as the example, then everything works well.
but if you use kurento-tutorial-js/kurento-recorder, you might found record not work.

https://github.com/Kurento/kurento-tutorial-js/pull/3

Below patch is work solution for it, but it was not accepted, strange thing.
https://github.com/Kurento/kurento-tutorial-js/pull/3/commits/ccab45818f3336639eec3c02a56f14209124901c

AWS: how to upload file to your s3 bucket via EvaporateJS in browser

Evaporate and config s3 bucket and IAM user

EvaporateJS
Config AWS s3 bucket
Managing Access Permissions to Your Amazon S3 Resources
User Access Key
In summary, create a s3 bucket, set up CORS settings and bucket policy.
 <CORSConfiguration>
     <CORSRule>
         <AllowedOrigin>https://*.yourdomain.com</AllowedOrigin>
         <AllowedOrigin>http://*.yourdomain.com</AllowedOrigin>
         <AllowedMethod>PUT</AllowedMethod>
         <AllowedMethod>POST</AllowedMethod>
         <AllowedMethod>DELETE</AllowedMethod>
         <AllowedMethod>GET</AllowedMethod>
         <ExposeHeader>ETag</ExposeHeader>
         <AllowedHeader>*</AllowedHeader>
     </CORSRule>
 </CORSConfiguration>
 
{
    "Version": "2012-10-17",
    "Id": "Policy145337ddwd",
    "Statement": [
        {
            "Sid": "",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::6681765859115:user/me"
            },
            "Action": [
                "s3:AbortMultipartUpload",
                "s3:ListMultipartUploadParts",
                "s3:GetObject",
                "s3:PutObject"
            ],
            "Resource": "arn:aws:s3:::mybucket/*"
        }
    ]
} 

Main codes

an example app created via 'create-reat-app' tool.
import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';

const Evaporate = require('evaporate');
const crypto = require('crypto');

class App extends Component {
  onFileSelected(files) {
    var config = {
      signerUrl: 'YOURSIGNEDURL',
      aws_key: 'YOURAWSKEY',
      bucket: 'YOURBUCKETNAME',
      cloudfront: true,
      computeContentMd5: true,
      cryptoMd5Method: function (data) {
        return crypto.createHash('md5').update(Buffer.from(data)).digest('base64');  
      },
      cryptoHexEncodedHash256: function (data) {
        return crypto.createHash('sha256').update(Buffer.from(data)).digest('hex');
      }
    };
    return Evaporate.create(config)
      .then(function (evaporate) {
         var addConfig = {
           name: files[0].name,
           file: files[0],
           progress: function (progressValue) {
             console.log('Progress', progressValue);
           },
           complete: function (_xhr, awsKey) {
             console.log('Complete!');
           },
         };
         var overrides = {
            bucket: 'recordingsfortalkmeup'
         };
         evaporate.add(addConfig, overrides)
           .then(function (awsObjectKey) {
                console.log('File successfully uploaded to:', awsObjectKey);
           },
           function (reason) {
             console.log('File did not upload sucessfully:', reason);
           });
      });
  }
  render() {
    return (
      <div className="App">
        <input type='file' onChange={(e) => this.onFileSelected(e.target.files)
 }/>
      </div>
    );
  }
}

export default App;

Signer URL

Create a lambda function to do the signature process.
Signing AWS Requests with Signature Version 4
'use strict';
console.log('Loading upload signature function');

const crypto = require("crypto");

function hmac(key, string){
    const hmac = crypto.createHmac('sha256', key);
    hmac.end(string);
    return hmac.read();
}

exports.handler = function(event, context, callback) {
    console.log(JSON.stringify(event));
    let to_sign = event.queryStringParameters.to_sign;
    let timestamp = event.queryStringParameters.datetime.substr(0, 8);
    console.log(to_sign);    
    console.log('date:', timestamp);
    console.log('aws secret key:', process.env.AWS_SECRET);
    console.log('region:', process.env.AWS_REGION);
    console.log('service:s3');
    let dateKey = hmac('AWS4' + process.env.AWS_SECRET, timestamp);
    let dateRegionKey = hmac(dateKey, process.env.AWS_REGION);
    let dateRegionServiceKey = hmac(dateRegionKey, 's3');
    let signingKey = hmac(dateRegionServiceKey, 'aws4_request');
    let signature = hmac(signingKey, to_sign).toString('hex');
    console.log('Created signature "' + signature + '" from ' + to_sign);
    var response = {
        statusCode: 200,
        headers: {
            "Access-Control-Allow-Origin" : "*",
            "Content-Type": "text/html"
        },
        body: signature
    };
    callback(null, response);
};
 
Build an rest API bind with this Lambda function and deploy it, then you will get an invoke url which will be your signer url.
Build an API Gateway API with Lambda Integration
APIGATEWAY

Test

upload
Everything is ok, you will find new uploaded file in your s3 bucket.
s3bucket

Download file from s3 bucket

AWS : how to get cognito user attributes in Lambda/cloud logic

Lambda function(js)

const AWS = require('aws-sdk');
const cognito = new AWS.CognitoIdentityServiceProvider();

function getUser(Username) {
    return new Promise((resolve, reject) => {
        cognito.adminGetUser({
            UserPoolId: process.env.COGNITO_USER_POOL_ID,
            Username: Username
        }, (err, data) => {
            if (err)
                reject(err.stack);
            else
                resolve(data.UserAttributes[2].Value);
        });
    });
}

async function getEmail(Username) {
  return await getUser(Username);
}

exports.handler = async (event) => {
    console.log(await getEmail('abcdef'));
    console.log(await getEmail('hijklmn'));
    console.log('done');
    return 'ok';
};
set Execution role with correct policy as below descripted.
set an environment 'COGNITO_USER_POOL_ID' with the user pool id.

Role policy

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": "cognito-idp:AdminGetUser",
            "Resource": "your user pool arn"
        }
    ]
}
userpool

Eexcution Result

Response:
"ok"

Request ID:
"a327a22f-0363-11e9-a897-6f0106540a1d"

Function Logs:
START RequestId: a327a22f-0363-11e9-a897-6f0106540a1d Version: $LATEST
2018-12-19T07:56:49.880Z    a327a22f-0363-11e9-a897-6f0106540a1d    errong.leng
@gmail.com
2018-12-19T07:56:50.056Z    a327a22f-0363-11e9-a897-6f0106540a1d    errong.leng
@hotmail.com
2018-12-19T07:56:50.056Z    a327a22f-0363-11e9-a897-6f0106540a1d    done
END RequestId: a327a22f-0363-11e9-a897-6f0106540a1d
REPORT RequestId: a327a22f-0363-11e9-a897-6f0106540a1d  Duration: 1149.09 ms
 Billed Duration: 1200 ms    Memory Size: 128 MB Max Memory Used: 30 MB

Refers

AWS.CognitoIdentityServiceProvider.adminGetUser

node.js download file from aws s3 bucket via http request with AWS Signature Version 4

Main codes

function getYMD(d) {
    let r = d.getFullYear().toString();
    let m = d.getUTCMonth() + 1;
    if (m < 10)
      m = "0" + m.toString();
    else
      m = m.toString();
    r = r + m;
    let day = d.getUTCDate();
    if (day < 10)
      day = "0" + day.toString();
    else
      day = day.toString();
    r = r + day;
    return r;
}

function getTZ(d) {
    let r = d.getFullYear().toString();
    let m = d.getUTCMonth() + 1;
    if (m < 10)
      m = "0" + m.toString();
    else
      m = m.toString();
    r = r + m;
    let day = d.getUTCDate();
    if (day < 10)
      day = "0" + day.toString();
    else
      day = day.toString();
    r = r + day + "T";
    let h = d.getUTCHours();
    if (h < 10)
      h = "0" + h.toString();
    else
      h = h.toString();
    r = r + h;
    let min = d.getUTCMinutes();
    if (min < 10)
      min = "0" + min.toString();
    else
      min = min.toString();
    r = r + min;
    let s = d.getUTCSeconds();
    if (s < 10)
      s = "0" + s.toString();
    else
      s = s.toString();
    r = r + s;
    r = r + "Z";
    return r;
}

const crypto = require('crypto');
function Hmac(key, string){
    const hmac = crypto.createHmac('sha256', key);
    hmac.end(string);
    return hmac.read();
}

function Signature(date, region, service, toSign) {
    let dateKey = Hmac('AWS4' + AWS_SECRETACCESS_KEY, date);
    let dateRegionKey = Hmac(dateKey, region);
    let dateRegionServiceKey = Hmac(dateRegionKey, service);
    let signingKey = Hmac(dateRegionServiceKey, 'aws4_request');
    let signature = Hmac(signingKey, toSign).toString('hex');
    return signature;
}

function stringToSign(timeStamp, scope, canonicalRequest) {
    var signParts = [];
    signParts.push('AWS4-HMAC-SHA256');
    signParts.push(timeStamp);
    signParts.push(scope);
    signParts.push(crypto.createHash('sha256').update(canonicalRequest).digest(
'hex'));
    var result = signParts.join('\n');
    console.log('string to sign');
    console.log(result);
    return result;
};

function canonicalRequest(method, uri, queryString, headers, signedHeaders, has
hedPayload) {
  var canonicalParts = [];
  canonicalParts.push(method);
  canonicalParts.push(uri);
  canonicalParts.push(queryString);
  canonicalParts.push(headers);
  canonicalParts.push(signedHeaders);
  canonicalParts.push(hashedPayload);
  var result = canonicalParts.join('\n');
  console.log('canonical request');
  console.log(result);
  return result;
}

function download(filepath) {
    let filename = filepath;
    let position = filename.lastIndexOf('/');
    if (position != -1) {
      filename = filename.substr(position+1);
    }
    let d = new Date();
    let dtz = getTZ(d);
    let dymd = getYMD(d);
    let scope = dymd + '/' + AWS_REGION + '/' + AWS_SERVICE + '/aws4_request';
    let uri = filepath;
    let queryString = "X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=" + en
codeURIComponent(AWS_KEY + "/" + scope) + "&X-Amz-Date=" + dtz + "&X-Amz-Expire
s=400&X-Amz-SignedHeaders=host&response-content-disposition=attachment%3Bfilena
me%3D" + filename;
    let headers = "host:s3.amazonaws.com";
    let signedHeaders = "";
    let hashedPayload = "host\nUNSIGNED-PAYLOAD";
    let request = canonicalRequest('GET', uri, queryString, headers, signedHead
ers, hashedPayload);
    let signature = Signature(dymd, AWS_REGION, AWS_SERVICE, stringToSign(dtz,
scope, request));
    let url = 'https://s3.amazonaws.com' + filepath + '?response-content-dispos
ition=attachment;filename=' + filename + '&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-A
mz-Credential=' + AWS_KEY + "/" + scope + '&X-Amz-Date=' + dtz + '&X-Amz-Expire
s=400&X-Amz-SignedHeaders=host&X-Amz-Signature=' + signature;
    console.log(url);
    var link = document.createElement('a');
    link.innerHTML = 'download';
    link.href = url;
    document.body.appendChild(link);
}

Usage

const AWS_KEY = 'your aws key';
const AWS_SECRETACCESS_KEY = 'your aws secret access key';
const AWS_REGION = 'your bucket region'; // default us-east-1
const AWS_SERVICE = 's3';

download('/yourbucket/objectkey');

examle

download

Refers

GET Object
Signing AWS Requests with Signature Version 4
Authenticating Requests: Using Query Parameters (AWS Signature Version 4)
Key point:
?response-content-disposition=attachment;filename=objectname
Using this header, browser will open an save dialog once the down load url is correct signed.
You will get a url link like:
https://s3.amazonaws.com/bucket/objectkey?response-content-disposition=attachment;filename=objectname&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAJWWH7EGSUBWE34IQ/20181218/us-east-1/s3/aws4_request&X-Amz-Date=20181218T053905Z&X-Amz-Expires=400&X-Amz-SignedHeaders=host&X-Amz-Signature=71d82d4a99338d777bfa3517315d34cad94619a9ca891222808e16dd02de7835

easy tab component, pure js + css, without react


This is tab example 1
Passion Organization Content Engagement this is passion pannel this is organization pannel this is engagement pannel
This is tab example 2
Passion Organization Content Engagement this is passion pannel 2 this is organization pannel 2 this is engagement pannel 2
end test
This is haha node

styles

<style>
.container {
    padding-right: 15px;
    padding-left: 15px;
    margin-right: auto;
    margin-left: auto;
}
.tabs {
    padding-left: 0;
    margin-bottom: 0;
    list-style: none; 
}
ol, ul {
    margin-top: 0;
    margin-bottom: 10px;
}
.tabs > li {    
    margin-bottom: -1px;
    position: relative;
    display: inline-block; 
 padding: 6px 12px;
 border-radius: 5px 5px 0 0;
 background-color: #aac4bd;
 border: 2px solid #7ea299;
 margin-right: 2px;
 color: #fff;
 cursor: pointer;
}
.tabs > li.active {
 background-color: #fbfbfc;
 border-bottom: 0;
 padding-bottom: 8px;
 color: #7ea299; 
}
.tabs > li:hover {
 background-color: #fbfbfc;
 color: #7ea299;  
}
.tabcontent {
 border: 2px solid #7ea299;
 -webkit-box-shadow: .055rem .055rem 1.11rem hsla(0,0%,8%,.27);
 box-shadow: .055rem .055rem 1.11rem hsla(0,0%,8%,.27);
 background-color: #fbfbfc;
 margin: -1px 0 0;
 padding: 4px; 
}
.tabcontent > .panel {
    display: none;
}
.tabcontent > .active {
    display: block;
}
</style>

use example

<div>
This is tab example 1
</div>
<Tabs>
<TabList>
  <Tab>Passion</Tab>
  <Tab>Organization Content</Tab>
  <Tab>Engagement</Tab>
</TabList>
<TabPanel>
  this is passion pannel
</TabPanel> 
<TabPanel>
this is organization pannel
</TabPanel>
<TabPanel>
this is engagement pannel
</TabPanel> 
</Tabs>
<div>
This is tab example 2
</div>
<Tabs>
<TabList>
  <Tab>Passion</Tab>
  <Tab>Organization Content</Tab>
  <Tab class='active'>Engagement</Tab>
</TabList>
<TabPanel>
  this is passion pannel 2
</TabPanel> 
<TabPanel>
this is organization pannel 2
</TabPanel>
<TabPanel>
this is engagement pannel 2
</TabPanel> 
</Tabs>
<div>
end test
</div>

javascript

<script type="text/javascript">
document.querySelectorAll('Tabs').forEach(function(tabs) {
  let pn = tabs.parentNode;
  let ct = document.createElement('div');
  ct.className = 'container';
  let ul = document.createElement('ul');
  ul.className = 'tabs';
  ct.appendChild(ul);
  let ai = 0;
  let index = 0;
  tabs.querySelectorAll('Tab').forEach(function(tab) {
    let li = document.createElement('li');
 li.className = tab.className;
 if (li.className.indexOf('active') != -1) {
   ai = index;
 }
 li.innerText = tab.innerText;
 li.id = index;
 li.onclick = function(e) {
   e = e || window.event;
      let target = e.target || e.srcElement;
   if (target.className.indexOf('active') != -1)
     return;  
   let ft = target.parentNode.querySelector('.active');
   ft.className = ft.className.replace('active', '');
   target.className = target.className + " active";
   let cp = target.parentNode.parentNode.querySelector('.tabcontent .active');
      cp.className = cp.className.replace('active', '');
   cp = target.parentNode.parentNode.querySelector('.tabcontent #panel'+target.id);
      cp.className = cp.className + " active";
   // target.parentNode.parentNode.style.display = 'block';
 }
    ul.appendChild(li); 
 index = index+1;
  });
  if (ai == 0) {
    if (ul.firstChild.className.indexOf('active') == -1) {
   ul.firstChild.className += ' active';
 }
  }
  let tc = document.createElement('div');
  tc.className = 'tabcontent';
  ct.appendChild(tc);
  index = 0;
  tabs.querySelectorAll('TabPanel').forEach(function(tabpanel) {
    let pl = document.createElement('div');
 pl.className = 'panel';
 pl.id = 'panel'+index;
 if (index == ai) {
   pl.className += ' active';
 }
 pl.innerHTML = tabpanel.innerHTML;
 tc.appendChild(pl);
 index = index+1;
  });
  pn.insertBefore(ct, tabs);
  pn.removeChild(tabs);
});
</script>

How to Record video use getUserMedia and MediaRecorder API

example


sample codes

  <div>
    <video id='camera'></video>
  </div>
  <script type='text/javascript'>
    var p = navigator.mediaDevices.getUserMedia({ audio: true, video: true });
    p.then(function(mediaStream) {
      var video = document.querySelector('video');
      video.src = window.URL.createObjectURL(mediaStream);
      video.onloadedmetadata = function() {
        video.muted = true;
        video.play();
      }
      var mediaRecorder = new MediaRecorder(mediaStream);
      var chunks = [];
      mediaRecorder.ondataavailable = function(e) {
        chunks.push(e.data);
      }
      mediaRecorder.onstop = function() {
        var blob = new Blob(chunks, {'type' : 'video/webm'});
        chunks = [];
        var hyperlink = document.createElement('a');
        hyperlink.href = URL.createObjectURL(blob);
        video.src = hyperlink.href;
        video.muted = false;
        video.controls = true;
        hyperlink.download = 'record.mp4';
        hyperlink.style = 'display:none;opacity:0;color:transparent;';
        (document.body || document.documentElement).appendChild(hyperlink);
if (typeof hyperlink.click === 'function') {
  hyperlink.click();
} else {
            hyperlink.target = '_blank';
            hyperlink.dispatchEvent(new MouseEvent('click', {
                view: window,
                bubbles: true,
                cancelable: true
            }));
        }     
    }
      mediaRecorder.start();      
      setTimeout(function() {
        mediaRecorder.stop();
        video.src = '';
        video.muted = false;
        video.stop();
      }, 10000);
    });
</script>

Ghost publishing platform sources: how hbs template engine is set to its site app

core js codes

in core/server/services/themes/active.js:

        // Set the views and engine         siteApp.set('views', this.path);         siteApp.engine('hbs', engine.configure(this.partialsPath)); 

Refers

https://expressjs.com/en/guide/using-template-engines.html
https://docs.ghost.org/api/handlebars-themes/

fixed: embedded-redis: Unable to run on macOS Sonoma

Issue you might see below error while trying to run embedded-redis for your testing on your macOS after you upgrade to Sonoma. java.la...