├── TODO ├── package.json ├── script └── run_ffmpeg.sh ├── server.js ├── README.md ├── basicPlayer.js ├── sipstack.js └── LICENSE /TODO: -------------------------------------------------------------------------------- 1 | - SIP invite : generateSDP // send invite // store callID info // check child process 2 | 3 | - Add CRD 4 | - restart ffmpeg on error + concat files 5 | - REST on file_id 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-sip-gw", 3 | "version": "1.0.0", 4 | "dependencies": 5 | { 6 | "express": "~4.0.0", 7 | "sdp-transform": "~2.0.0", 8 | "uuid":"*" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /script/run_ffmpeg.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Script to run rtp ffmpeg 3 | local_sdp=$1 4 | remote_audio=$2 5 | remote_video=$3 6 | 7 | exec=./ffmpeg 8 | 9 | echo $local_sdp 10 | 11 | #echo $local_sdp > file.sdp 12 | 13 | echo $remote_host 14 | 15 | $exec -analyzeduration 100000000 -probesize 1000000000 -protocol_whitelist "rtp,sdp,file,udp,tcp" -re -f sdp -i file.sdp -s 640x480 -vcodec libx264 -tune zerolatency -acodec aac -y -f mp4 test.mp4 & 16 | #$exec -re -i test_vid.mp4 -vn -acodec libopus -f rtp $remote_audio & 17 | #$exec -re -i test_vid.mp4 -vcodec libx264 -tune zerolatency -an -f rtp $remote_video 18 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | 2 | var express = require('express'); 3 | var app = express(); 4 | var sip = require('./sipstack.js'); 5 | var uuid = require('uuid/v1'); 6 | var calls = {}; 7 | 8 | sip.init(); 9 | 10 | app.get('/sip/invite', function (req, res) { 11 | var to = req.query.to; 12 | var from = req.query.from; 13 | var logoUrl = req.query.logourl; 14 | var recordFormat = req.query.recordformat; 15 | var callid = uuid(); 16 | console.log("Sip invite "+ to +" "+ from +" "+ logoUrl + " " + recordFormat); 17 | 18 | var response = {}; 19 | response.callid=callid; 20 | response.recordUrl="/ff/ff/"+callid+"."+recordFormat; 21 | 22 | 23 | 24 | 25 | res.send(JSON.stringify(response)); 26 | }); 27 | 28 | function createSipCall(sessionId,from,to,rtpEndpoint,callback){ 29 | rtpEndpoint.generateOffer(function(error, sdpOffer) { 30 | var modSdp = replace_ip(sdpOffer); 31 | sip.invite (sessionId,from,to,modSdp,function (error,remoteSdp){ 32 | if (error){ 33 | return callback(error); 34 | } 35 | rtpEndpoint.processAnswer(remoteSdp,function(error){ 36 | if (error){ 37 | return callback(error); 38 | } 39 | // Insert EnCall timeout 40 | setTimeout(function(){ sip.bye(sessionId);stopFromBye(sessionId);},config.maxCallSeconds*1000); 41 | return callback(null); 42 | }); 43 | }); 44 | }); 45 | } 46 | 47 | function generateLocalSDP(){ 48 | 49 | } 50 | 51 | app.get('/sip/bye', function (req, res) { 52 | var callId = req.query.callid; 53 | console.log("Sip bye "+ callId); 54 | res.send("OK"); 55 | }); 56 | 57 | 58 | function stop(sessionId) { 59 | sip.bye(sessionId); 60 | if (sessions[sessionId]) { 61 | var pipeline = sessions[sessionId].pipeline; 62 | if (pipeline != undefined){ 63 | console.info('Releasing pipeline'); 64 | pipeline.release(); 65 | } 66 | delete sessions[sessionId]; 67 | delete candidatesQueue[sessionId]; 68 | } 69 | } 70 | 71 | var server = app.listen(3000,function(){ 72 | console.log("We have started our server on port 3000"); 73 | }); 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SIPLightMRF 2 | Simple customisable media player and recorder SIP endpoint 3 | 4 | ## Synopsis 5 | 6 | This project is a SIP Video PLayer /Recorder. It uses [drachtio](https://github.com/davehorton/drachtio) for SIP signaling and [FFMPEG](https://www.kurento.org/) for the Media manipulation. 7 | 8 | ## Motivation 9 | 10 | This recorder is made to connect to any classic SIP endpoint like Softphone, PABX or MCU and record remote stream. 11 | It was firstly designed to work with Asterisk and it works with it. 12 | 13 | ## Installation on Ubuntu 14.04 14 | 15 | 16 | First , install NodeJS 17 | ```bash 18 | sudo apt-get install nodejs 19 | ``` 20 | 21 | Then install, [drachtio-server](https://github.com/davehorton/drachtio-server) : 22 | 23 | ```bash 24 | git clone git://github.com/davehorton/drachtio-server.git && cd drachtio-server 25 | git submodule update --init --recursive 26 | ./bootstrap.sh 27 | mkdir build && cd $_ 28 | ../configure CPPFLAGS='-DNDEBUG' 29 | make 30 | sudo make install 31 | ``` 32 | 33 | Then install FFMPEG 34 | ```bash 35 | apt-get install ffmpeg 36 | ``` 37 | 38 | 39 | And finally, install node modules : 40 | ```bash 41 | npm install 42 | ``` 43 | 44 | ## Server Configuration 45 | Change config.serverPublicIP in file config.js to expose external public IP used for SIP sdp generation. 46 | * config.ffmpegPath Path to your FFMPEG bin 47 | 48 | ## To run 49 | ```bash 50 | node server.js 51 | ``` 52 | 53 | * Start a record with Send HTTP GET request https://localhost:8443/sip/invite?to=bob@192.168.0.17&from=recorder@127.0.0.1&logoUrl=http://dfdfdffd/ 54 | return callid 55 | * Stop a record with Send HTTP GET request https://localhost:8443/sip/bye?callid=XXXXXXXXXXX 56 | * Stop all records with Send HTTP GET request https://localhost:8443/sip/globalbye 57 | * Get recorded file URL https://localhost:8443/record/callid 58 | 59 | * Get current record list https://localhost:8443/sip/status 60 | * Get all record list https://localhost:8443/record/status 61 | 62 | 63 | ## Contributors 64 | Damien Fétis 65 | 66 | ## License 67 | 68 | Licensed under the Apache License, Version 2.0 (the "License"); 69 | you may not use this file except in compliance with the License. 70 | You may obtain a copy of the License at 71 | 72 | http://www.apache.org/licenses/LICENSE-2.0 73 | 74 | Unless required by applicable law or agreed to in writing, software 75 | distributed under the License is distributed on an "AS IS" BASIS, 76 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 77 | See the License for the specific language governing permissions and 78 | limitations under the License. 79 | -------------------------------------------------------------------------------- /basicPlayer.js: -------------------------------------------------------------------------------- 1 | var drachtio = require('drachtio'); 2 | var appSip = drachtio() ; 3 | var fs = require('fs') ; 4 | var argv = require('minimist')(process.argv.slice(2)); 5 | var debug = require('debug')('basic') ; 6 | var transform = require('sdp-transform'); 7 | var sipServerConnected = false; 8 | var kill = require('tree-kill'); 9 | 10 | var myFakeSDP = "v=0\r\n\ 11 | o=sipGWNode 786 3205 IN IP4 192.168.0.17\r\n\ 12 | s=Talk\r\n\ 13 | c=IN IP4 192.168.0.17\r\n\ 14 | t=0 0\r\n\ 15 | m=audio 7078 RTP/AVP 97 0 8 101 99 100\r\n\ 16 | a=rtpmap:97 opus/48000/2\r\n\ 17 | a=fmtp:97 sprop-stereo=1\r\n\ 18 | a=rtpmap:101 telephone-event/48000\r\n\ 19 | a=rtpmap:99 telephone-event/16000\r\n\ 20 | a=rtpmap:100 telephone-event/8000\r\n\ 21 | m=video 9078 RTP/AVP 96\r\n\ 22 | a=rtpmap:96 H264/90000\r\n\ 23 | "; 24 | 25 | var myFakeSDP2 = "v=0\n\ 26 | o=sipGWNode 786 3205 IN IP4 192.168.0.17\n\ 27 | s=Talk\n\ 28 | c=IN IP4 192.168.0.17\n\ 29 | t=0 0\n\ 30 | m=audio 7078 RTP/AVP 97 0 8 101 99 100\n\ 31 | a=rtpmap:97 opus/48000/2\n\ 32 | a=fmtp:97 sprop-stereo=1\n\ 33 | a=rtpmap:101 telephone-event/48000\n\ 34 | a=rtpmap:99 telephone-event/16000\n\ 35 | a=rtpmap:100 telephone-event/8000\n\ 36 | m=video 9078 RTP/AVP 96\n\ 37 | a=rtpmap:96 H264/90000\n\ 38 | "; 39 | 40 | var sessions = {}; 41 | 42 | appSip.use('invite', function( req, res, next){ 43 | debug('hey Im in middleware!') ; 44 | next() ; 45 | }) ; 46 | appSip.use('invite', function( req, res, next){ 47 | debug('hey Im in middleware!') ; 48 | throw new Error('whoopse!') ; 49 | }) ; 50 | appSip.use('invite', function( req, res){ 51 | res.send(486) ; 52 | }) ; 53 | appSip.use('invite', function( err, req, res, next){ 54 | debug('got eror: ', err) ; 55 | res.send(500, 'My bad') ; 56 | }) ; 57 | 58 | 59 | appSip.use('bye', function( req, res){ 60 | console.log('BYE recieved: %s', JSON.stringify(res) ) ; 61 | var index = res.msg.headers['call-id']; 62 | console.log('BYE recieved: %s index %s' , JSON.stringify(res),index ) ; 63 | // console.log('Sessions : %s', JSON.stringify(sessions) ) ; 64 | 65 | var child = sessions[index]; 66 | if (child != undefined){ 67 | //kill(child.pid); 68 | child.kill(); 69 | } 70 | res.send(200) ; 71 | }) ; 72 | 73 | 74 | appSip.connect({ 75 | host: '127.0.0.1', 76 | port: 9022, 77 | secret: 'cymru', 78 | methods: ['invite','bye','option'], 79 | // set: { 80 | // 'api logger': fs.createWriteStream(argv.apiTrace) 81 | // } 82 | }, 83 | function(err, hostport){ 84 | if( err ) throw err ; 85 | console.log("Connected to SIP server"); 86 | sipServerConnected = true; 87 | } 88 | ) ; 89 | 90 | /*-----------------------*/ 91 | var spawn = require('child_process').spawn; 92 | var express = require('express'); 93 | var app = express(); 94 | app.get('/invite', function (req, res) { 95 | var sipDest = req.query.to; 96 | var httpRes =res; 97 | console.log("Send invite to "+sipDest); 98 | //res.send('Send invite to '+sipDest); 99 | if (sipServerConnected){ 100 | appSip.request({ 101 | uri: 'sip:'+sipDest, 102 | method: 'INVITE', 103 | headers: { 104 | 'User-Agent': 'dracht.io' 105 | }, 106 | body: myFakeSDP 107 | }, 108 | function( err, req ){ 109 | if( err ) { 110 | console.log('Error '+ err ) ; 111 | throw err; 112 | } 113 | console.log('sent request: %s', JSON.stringify(req) ) ; 114 | 115 | req.on('response', function(res,ack){ 116 | console.log('received response with status: %s', res.status) ; 117 | console.log('recieved response: %s', JSON.stringify(res) ) ; 118 | 119 | if( res.status >= 200 ) { 120 | var remoteSdpStr = res.body; 121 | console.log('recieved response: %s', remoteSdpStr); 122 | var remoteSdpJson = transform.parse(remoteSdpStr); 123 | 124 | console.log('recieved response: %s',remoteSdpJson); 125 | 126 | console.log('remote host IP : %s', getConnectionIp(remoteSdpJson)) ; 127 | console.log('remote audio port : %s', getPortByType(remoteSdpJson,"audio")) ; 128 | console.log('remote video port : %s', getPortByType(remoteSdpJson,"video")) ; 129 | 130 | var remoteAudio = "rtp://"+getConnectionIp(remoteSdpJson)+":"+getPortByType(remoteSdpJson,"audio") 131 | var remoteVideo = "rtp://"+getConnectionIp(remoteSdpJson)+":"+getPortByType(remoteSdpJson,"video") 132 | 133 | var child = spawn('./run_ffmpeg.sh', [ 134 | myFakeSDP2, 135 | remoteAudio, 136 | remoteVideo 137 | ]); 138 | console.log(req.msg.headers); 139 | var callId = req.msg.headers["call-id"]; 140 | httpRes.send(callId); 141 | sessions[callId]=child; 142 | 143 | //console.log('Sessions : %s', JSON.stringify(sessions) ) ; 144 | 145 | child.stdout.on('data', (data) => { 146 | console.log(`stdout: ${data}`); 147 | }); 148 | 149 | child.stderr.on('data', (data) => { 150 | console.log(`stderr: ${data}`); 151 | }); 152 | 153 | child.on('close', (code) => { 154 | console.log(`child process exited with code ${code}`); 155 | }); 156 | 157 | ack() ; 158 | } 159 | }) ; 160 | } 161 | ); 162 | } 163 | }); 164 | 165 | function getConnectionIp (sdpJson){ 166 | return sdpJson.connection.ip; 167 | } 168 | 169 | 170 | function getPortByType (sdpJson,type){ 171 | var media = sdpJson.media; 172 | for (var i = 0 ; i < media.length ; i++){ 173 | if (media[i].type == type){ 174 | return media[i].port; 175 | } 176 | } 177 | return 0; 178 | } 179 | 180 | app.get('/bye', function (req, res) { 181 | var sipDest = req.query.callId; 182 | var child = sessions[callId]; 183 | if(child!=undefined){ 184 | kill(child.pid); 185 | } 186 | }); 187 | 188 | var server = app.listen(3000,function(){ 189 | console.log("We have started our server on port 3000"); 190 | }); 191 | -------------------------------------------------------------------------------- /sipstack.js: -------------------------------------------------------------------------------- 1 | var drachtio = require('drachtio'); 2 | var appSip = drachtio() ; 3 | var fs = require('fs') ; 4 | var debug = require('debug')('basic') ; 5 | var transform = require('sdp-transform'); 6 | var kill = require('tree-kill'); 7 | var config = require('./config'); 8 | 9 | function getConnectionIp (sdpJson){ 10 | return sdpJson.connection.ip; 11 | } 12 | 13 | 14 | function getPortByType (sdpJson,type){ 15 | var media = sdpJson.media; 16 | for (var i = 0 ; i < media.length ; i++){ 17 | if (media[i].type == type){ 18 | return media[i].port; 19 | } 20 | } 21 | return 0; 22 | } 23 | 24 | 25 | 26 | var SipStack = function () {}; 27 | 28 | SipStack.appSip = drachtio(); 29 | 30 | SipStack.dialogs = {}; 31 | SipStack.sessionIds = {}; 32 | SipStack.sessionIdByCalls = {}; 33 | SipStack.requests = {}; 34 | SipStack.prototype.log = function () { 35 | console.log('doo!'); 36 | } 37 | 38 | SipStack.kurentoPipelineRelease = function (sessionId) { 39 | console.log('Bad callback end') ; 40 | 41 | }; 42 | 43 | SipStack.appSip.use('bye', function( req, res){ 44 | console.log('BYE recieved: %s', JSON.stringify(res) ) ; 45 | var callId = res.msg.headers['call-id']; 46 | console.log('BYE recieved: %s index %s' , JSON.stringify(res),callId ) ; 47 | // get sessionId 48 | var sessionId = SipStack.sessionIdByCalls[callId]; 49 | SipStack.kurentoPipelineRelease(sessionId); 50 | var dialog = SipStack.dialogs[sessionId]; 51 | 52 | delete SipStack.sessionIds[dialog]; 53 | delete SipStack.dialogs[sessionId]; 54 | delete SipStack.sessionIdByCalls[callId]; 55 | res.send(200) ; 56 | }) ; 57 | 58 | SipStack.prototype.init = function (callback){ 59 | SipStack.sipServerConnected = false; 60 | SipStack.kurentoPipelineRelease = callback; 61 | SipStack.sessions = {}; 62 | SipStack.appSip.connect(config.drachtio, 63 | function(err, hostport){ 64 | if( err ) throw err ; 65 | console.log("Connected to SIP server"); 66 | sipServerConnected = true; 67 | } 68 | ) ; 69 | } 70 | 71 | SipStack.prototype.invite = function (sessionId,from,to,localSDP,callback){ 72 | var sipDest = to; 73 | console.log("Send invite to "+ sipDest); 74 | if (sipServerConnected){ 75 | SipStack.appSip.request({ 76 | uri: "sip:"+sipDest, 77 | method: 'INVITE', 78 | headers: { 79 | 'User-Agent': 'dracht.io', 80 | 'from': "sip:"+from 81 | }, 82 | body: localSDP 83 | }, 84 | function( err, req ){ 85 | if( err ) { 86 | console.log('Error '+ err ) ; 87 | return callback("reject",null); 88 | } 89 | console.log('sent request: %s', JSON.stringify(req) ) ; 90 | 91 | req.on('response', function(res,ack){ 92 | console.log('received response with status: %s', res.status) ; 93 | console.log('recieved response: %s', JSON.stringify(res) ) ; 94 | 95 | if( res.status == 200) { 96 | var remoteSdpStr = res.body; 97 | remoteSdpStr+="\na=ptime:20"; 98 | console.log('recieved response: %s', remoteSdpStr); 99 | console.log(req.msg.headers); 100 | console.log('dialogId recv: ', res.stackDialogId); 101 | SipStack.dialogs[sessionId] = res.stackDialogId ; 102 | SipStack.sessionIds[res.stackDialogId] = sessionId; 103 | var callId = req.msg.headers["call-id"]; 104 | SipStack.sessionIdByCalls[callId] = sessionId; 105 | delete SipStack.requests[sessionId]; 106 | ack() ; 107 | console.log("Ack sended"); 108 | return callback(null,remoteSdpStr); 109 | } 110 | 111 | else if (res.status >=300){ 112 | console.log('Invite rejected'); 113 | SipStack.kurentoPipelineRelease(sessionId); 114 | return callback("reject",null); 115 | } 116 | else if (res.status < 200){ 117 | console.log('Intermediate repsonse'); 118 | SipStack.requests[sessionId] = req ; 119 | } 120 | 121 | }) ; 122 | } 123 | ); 124 | return ""; 125 | } 126 | return ""; 127 | }; 128 | 129 | SipStack.prototype.bye = function (sessionId,sdpLocal) { 130 | 131 | if (SipStack.requests[sessionId]!=undefined){ 132 | SipStack.requests[sessionId].cancel(); 133 | } 134 | else { 135 | var dialog = SipStack.dialogs[sessionId]; 136 | if (dialog != undefined){ 137 | SipStack.appSip.request({ 138 | method: 'BYE', 139 | stackDialogId: dialog 140 | }, function(err, req){ 141 | if( err || req == undefined) { 142 | console.log(err); 143 | return; 144 | } 145 | var callId = req.msg.headers["call-id"]; 146 | delete SipStack.sessionIds[dialog]; 147 | delete SipStack.dialogs[sessionId]; 148 | delete SipStack.sessionIdByCalls[callId]; 149 | req.on('response', function(response){ 150 | console.log('BYE '+response.status); 151 | }); 152 | }) ; 153 | } 154 | } 155 | }; 156 | 157 | SipStack.prototype.infoDtmf = function (sessionId,dtmf) { 158 | 159 | var dialog = SipStack.dialogs[sessionId]; 160 | if (dialog != undefined){ 161 | var messageBody="Signal="+dtmf+"\r\nDuration=160\r\n"; 162 | SipStack.appSip.request({ 163 | method: 'INFO', 164 | stackDialogId: dialog, 165 | headers: { 166 | 'User-Agent': 'dracht.io', 167 | 'Content-Type': 'application/dtmf-relay' 168 | }, 169 | body: messageBody 170 | }, function(err, req){ 171 | 172 | if(req!=undefined) 173 | console.log('sent request: %s', JSON.stringify(req) ) ; 174 | 175 | 176 | if( err || req == undefined) { 177 | console.log(err); 178 | console.log(req); 179 | return; 180 | } 181 | req.on('response', function(response){ 182 | console.log('INFO '+response.status); 183 | }); 184 | }) ; 185 | } 186 | }; 187 | 188 | 189 | SipStack.prototype.response = function (sessionId) { 190 | 191 | }; 192 | 193 | SipStack.prototype.registerWebRTCEndpoint = function (from) { 194 | 195 | }; 196 | 197 | SipStack.prototype.registerSIP = function (from,host,password) { 198 | 199 | }; 200 | 201 | module.exports = new SipStack(); 202 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------