webrtc-troubleshoot
Advanced tools
Comparing version 0.0.3 to 0.1.0
{ | ||
"name": "webrtc-troubleshoot", | ||
"version": "0.0.3", | ||
"version": "0.1.0", | ||
"description": "A way to add webrtc troubleshooting to your app", | ||
"directories": { | ||
"test": "tests" | ||
}, | ||
"main": "index.js", | ||
"scripts": { | ||
"build": "browserify -t [ babelify --presets [ es2015 ] ] --s WebRTCTroubleshooter src/index.js -o dist/webrtc-troubleshooter.bundle.js" | ||
"build": "browserify -t [ babelify --presets [ es2015 ] ] --s WebRTCTroubleshooter src/index.js -o dist/webrtc-troubleshooter.bundle.js", | ||
"test": "node_modules/.bin/semistandard src/utils/**/*.js test-page/**/*.js" | ||
}, | ||
@@ -17,7 +15,5 @@ "repository": "https://github.com/adflynn/webrtc-troubleshooter", | ||
"devDependencies": { | ||
"attachmediastream": "https://github.com/otalk/attachMediaStream", | ||
"babel-cli": "^6.7.7", | ||
"babel-preset-es2015": "^6.6.0", | ||
"babelify": "^7.2.0", | ||
"babel-preset-es2015": "^6.6.0", | ||
"lodash": "^4.6.1", | ||
"localmedia": "https://github.com/xdumaine/localmedia.git", | ||
@@ -24,0 +20,0 @@ "rtcpeerconnection": "https://github.com/otalk/RTCPeerConnection.git" |
@@ -8,12 +8,13 @@ # webrtc-troubleshooter | ||
TODO: | ||
* [ ] create non-ember based webrtc tester | ||
* [ ] create dummy page for testing | ||
* [x] create non-ember based webrtc tester | ||
* [x] create dummy page for testing | ||
* [ ] publish as bower package | ||
# Installation | ||
# Develop | ||
* `git clone` this repository | ||
* `npm install` | ||
* `bower install` | ||
* Serve with your favorite [stupid server](https://www.npmjs.com/package/stupid-server) | ||
* `npm test` | ||
@@ -24,2 +25,2 @@ | ||
test-page/index.html | ||
for console output of 6 tests to ensure webrtc is properly functioning | ||
for console output of 6 tests to ensure webrtc is properly functioning |
@@ -1,19 +0,12 @@ | ||
import { TestSuite } from './utils/TestSuite'; | ||
import { | ||
AudioTest, | ||
VideoTest, | ||
ConnectivityTest, | ||
AdvancedCameraTest, | ||
ThroughputTest, | ||
VideoBandwidthTest | ||
} from './utils/tests/defaultTests'; | ||
import TestSuite from './utils/TestSuite'; | ||
import { AudioTest, VideoTest, ConnectivityTest, AdvancedCameraTest, ThroughputTest, VideoBandwidthTest } from './defaultTests'; | ||
export default { | ||
TestSuite, | ||
AudioTest, | ||
VideoTest, | ||
ConnectivityTest, | ||
AdvancedCameraTest, | ||
ThroughputTest, | ||
VideoBandwidthTest | ||
TestSuite, | ||
AudioTest, | ||
VideoTest, | ||
ConnectivityTest, | ||
AdvancedCameraTest, | ||
ThroughputTest, | ||
VideoBandwidthTest | ||
}; |
@@ -1,3 +0,2 @@ | ||
/* global _ */ | ||
class TestSuite { | ||
export default class TestSuite { | ||
constructor (options) { | ||
@@ -15,26 +14,31 @@ options = options || {}; | ||
runNextTest (done) { | ||
start () { | ||
return new Promise((resolve, reject) => { | ||
return this.runNextTest().then(resolve, reject); | ||
}); | ||
} | ||
runNextTest () { | ||
this.running = true; | ||
var test = this.queue.shift(); | ||
const test = this.queue.shift(); | ||
if (!test) { | ||
this.running = false; | ||
this.allTestsComplete = true; | ||
return done(); | ||
return Promise.resolve(); | ||
} | ||
this.activeTest = test; | ||
this.logger.log('webrtc-troubleshooter: Starting ' + test.name); | ||
// TODO: There is some repeating functionality here that could be extracted. | ||
test.start().then(() => { | ||
test.callback(null); | ||
this.logger.log('Starting ' + test.name); | ||
const next = () => { | ||
test.running = false; | ||
test.destroy(); | ||
this.runNextTest(done); | ||
}).catch((err) => { | ||
test.callback(err, test.log); | ||
test.running = false; | ||
test.destroy(); | ||
this.runNextTest(done); | ||
return this.runNextTest(); | ||
}; | ||
return test.start().then(() => { | ||
return next(); | ||
}, (err) => { | ||
this.logger.error('Test failure', err, test); | ||
return next(); | ||
}); | ||
@@ -48,23 +52,1 @@ } | ||
} | ||
class Test { | ||
constructor (options, callback) { | ||
this.options = options || {}; | ||
this.callback = callback || _.noop; | ||
this.logger = this.options.logger || console; | ||
} | ||
start () { | ||
this.timeout = window.setTimeout(() => { | ||
if (this.reject) { | ||
this.reject('timeout'); | ||
} | ||
}, 30000); | ||
} | ||
destroy () { | ||
window.clearTimeout(this.timeout); | ||
} | ||
} | ||
export { TestSuite, Test }; |
@@ -1,4 +0,3 @@ | ||
// adapted from https://github.com/webrtc/testrtc | ||
class VideoFrameChecker { | ||
import Ssim from './Ssim.js'; | ||
export default class VideoFrameChecker { | ||
constructor (videoElement) { | ||
@@ -11,51 +10,51 @@ this.frameStats = { | ||
this.running_ = true; | ||
this.running = true; | ||
this.nonBlackPixelLumaThreshold = 20; | ||
this.previousFrame_ = []; | ||
this.previousFrame = []; | ||
this.identicalFrameSsimThreshold = 0.985; | ||
this.frameComparator = new Ssim(); | ||
this.canvas_ = document.createElement('canvas'); | ||
this.videoElement_ = videoElement; | ||
this.listener_ = this.checkVideoFrame_.bind(this); | ||
this.videoElement_.addEventListener('play', this.listener_, false); | ||
this.canvas = document.createElement('canvas'); | ||
this.videoElement = videoElement; | ||
this.listener = this.checkVideoFrame.bind(this); | ||
this.videoElement.addEventListener('play', this.listener, false); | ||
} | ||
stop () { | ||
this.videoElement_.removeEventListener('play', this.listener_); | ||
this.running_ = false; | ||
this.videoElement.removeEventListener('play', this.listener); | ||
this.running = false; | ||
} | ||
getCurrentImageData_ () { | ||
this.canvas_.width = this.videoElement_.width; | ||
this.canvas_.height = this.videoElement_.height; | ||
getCurrentImageData () { | ||
this.canvas.width = this.videoElement.width; | ||
this.canvas.height = this.videoElement.height; | ||
var context = this.canvas_.getContext('2d'); | ||
context.drawImage(this.videoElement_, 0, 0, this.canvas_.width, | ||
this.canvas_.height); | ||
return context.getImageData(0, 0, this.canvas_.width, this.canvas_.height); | ||
var context = this.canvas.getContext('2d'); | ||
context.drawImage(this.videoElement, 0, 0, this.canvas.width, | ||
this.canvas.height); | ||
return context.getImageData(0, 0, this.canvas.width, this.canvas.height); | ||
} | ||
checkVideoFrame_ () { | ||
if (!this.running_) { | ||
checkVideoFrame () { | ||
if (!this.running) { | ||
return; | ||
} | ||
if (this.videoElement_.ended) { | ||
if (this.videoElement.ended) { | ||
return; | ||
} | ||
var imageData = this.getCurrentImageData_(); | ||
var imageData = this.getCurrentImageData(); | ||
if (this.isBlackFrame_(imageData.data, imageData.data.length)) { | ||
if (this.isBlackFrame(imageData.data, imageData.data.length)) { | ||
this.frameStats.numBlackFrames++; | ||
} | ||
if (this.frameComparator.calculate(this.previousFrame_, imageData.data) > | ||
if (this.frameComparator.calculate(this.previousFrame, imageData.data) > | ||
this.identicalFrameSsimThreshold) { | ||
this.frameStats.numFrozenFrames++; | ||
} | ||
this.previousFrame_ = imageData.data; | ||
this.previousFrame = imageData.data; | ||
this.frameStats.numFrames++; | ||
setTimeout(this.checkVideoFrame_.bind(this), 20); | ||
setTimeout(this.checkVideoFrame.bind(this), 20); | ||
} | ||
isBlackFrame_ (data, length) { | ||
isBlackFrame (data, length) { | ||
// TODO: Use a statistical, histogram-based detection. | ||
@@ -75,136 +74,1 @@ var thresh = this.nonBlackPixelLumaThreshold; | ||
} | ||
VideoFrameChecker.prototype = { | ||
stop: function () { | ||
this.videoElement_.removeEventListener('play', this.listener_); | ||
this.running_ = false; | ||
}, | ||
getCurrentImageData_: function () { | ||
this.canvas_.width = this.videoElement_.width; | ||
this.canvas_.height = this.videoElement_.height; | ||
var context = this.canvas_.getContext('2d'); | ||
context.drawImage(this.videoElement_, 0, 0, this.canvas_.width, | ||
this.canvas_.height); | ||
return context.getImageData(0, 0, this.canvas_.width, this.canvas_.height); | ||
}, | ||
checkVideoFrame_: function () { | ||
if (!this.running_) { | ||
return; | ||
} | ||
if (this.videoElement_.ended) { | ||
return; | ||
} | ||
var imageData = this.getCurrentImageData_(); | ||
if (this.isBlackFrame_(imageData.data, imageData.data.length)) { | ||
this.frameStats.numBlackFrames++; | ||
} | ||
if (this.frameComparator.calculate(this.previousFrame_, imageData.data) > | ||
this.identicalFrameSsimThreshold) { | ||
this.frameStats.numFrozenFrames++; | ||
} | ||
this.previousFrame_ = imageData.data; | ||
this.frameStats.numFrames++; | ||
setTimeout(this.checkVideoFrame_.bind(this), 20); | ||
}, | ||
isBlackFrame_: function (data, length) { | ||
// TODO: Use a statistical, histogram-based detection. | ||
var thresh = this.nonBlackPixelLumaThreshold; | ||
var accuLuma = 0; | ||
for (var i = 4; i < length; i += 4) { | ||
// Use Luma as in Rec. 709: Y′709 = 0.21R + 0.72G + 0.07B | ||
accuLuma += 0.21 * data[i] + 0.72 * data[i + 1] + 0.07 * data[i + 2]; | ||
// Early termination if the average Luma so far is bright enough. | ||
if (accuLuma > (thresh * i / 4)) { | ||
return false; | ||
} | ||
} | ||
return true; | ||
} | ||
}; | ||
/* This is an implementation of the algorithm for calculating the Structural | ||
* SIMilarity (SSIM) index between two images. Please refer to the article [1], | ||
* the website [2] and/or the Wikipedia article [3]. This code takes the value | ||
* of the constants C1 and C2 from the Matlab implementation in [4]. | ||
* | ||
* [1] Z. Wang, A. C. Bovik, H. R. Sheikh, and E. P. Simoncelli, "Image quality | ||
* assessment: From error measurement to structural similarity", | ||
* IEEE Transactions on Image Processing, vol. 13, no. 1, Jan. 2004. | ||
* [2] http://www.cns.nyu.edu/~lcv/ssim/ | ||
* [3] http://en.wikipedia.org/wiki/Structural_similarity | ||
* [4] http://www.cns.nyu.edu/~lcv/ssim/ssim_index.m | ||
*/ | ||
class Ssim { | ||
// Implementation of Eq.2, a simple average of a vector and Eq.4., except the | ||
// square root. The latter is actually an unbiased estimate of the variance, | ||
// not the exact variance. | ||
statistics (a) { | ||
var accu = 0; | ||
var i; | ||
for (i = 0; i < a.length; ++i) { | ||
accu += a[i]; | ||
} | ||
var meanA = accu / (a.length - 1); | ||
var diff = 0; | ||
for (i = 1; i < a.length; ++i) { | ||
diff = a[i - 1] - meanA; | ||
accu += a[i] + (diff * diff); | ||
} | ||
return {mean: meanA, variance: accu / a.length}; | ||
} | ||
// Implementation of Eq.11., cov(Y, Z) = E((Y - uY), (Z - uZ)). | ||
covariance (a, b, meanA, meanB) { | ||
var accu = 0; | ||
for (var i = 0; i < a.length; i += 1) { | ||
accu += (a[i] - meanA) * (b[i] - meanB); | ||
} | ||
return accu / a.length; | ||
} | ||
calculate (x, y) { | ||
if (x.length !== y.length) { | ||
return 0; | ||
} | ||
// Values of the constants come from the Matlab code referred before. | ||
var K1 = 0.01; | ||
var K2 = 0.03; | ||
var L = 255; | ||
var C1 = (K1 * L) * (K1 * L); | ||
var C2 = (K2 * L) * (K2 * L); | ||
var C3 = C2 / 2; | ||
var statsX = this.statistics(x); | ||
var muX = statsX.mean; | ||
var sigmaX2 = statsX.variance; | ||
var sigmaX = Math.sqrt(sigmaX2); | ||
var statsY = this.statistics(y); | ||
var muY = statsY.mean; | ||
var sigmaY2 = statsY.variance; | ||
var sigmaY = Math.sqrt(sigmaY2); | ||
var sigmaXy = this.covariance(x, y, muX, muY); | ||
// Implementation of Eq.6. | ||
var luminance = (2 * muX * muY + C1) / | ||
((muX * muX) + (muY * muY) + C1); | ||
// Implementation of Eq.10. | ||
var structure = (sigmaXy + C3) / (sigmaX * sigmaY + C3); | ||
// Implementation of Eq.9. | ||
var contrast = (2 * sigmaX * sigmaY + C2) / (sigmaX2 + sigmaY2 + C2); | ||
// Implementation of Eq.12. | ||
return luminance * contrast * structure; | ||
} | ||
} | ||
export default VideoFrameChecker; |
@@ -9,10 +9,10 @@ /* global RTCPeerConnection, mozRTCPeerConnection */ | ||
this.pc1.addEventListener('icecandidate', this.onIceCandidate_.bind(this, this.pc2)); | ||
this.pc2.addEventListener('icecandidate', this.onIceCandidate_.bind(this, this.pc1)); | ||
this.pc1.addEventListener('icecandidate', this.onIceCandidate.bind(this, this.pc2)); | ||
this.pc2.addEventListener('icecandidate', this.onIceCandidate.bind(this, this.pc1)); | ||
this.iceCandidateFilter_ = WebrtcCall.noFilter; | ||
this.iceCandidateFilter = WebrtcCall.noFilter; | ||
} | ||
establishConnection () { | ||
this.pc1.createOffer(this.gotOffer_.bind(this), console.error.bind(console)); | ||
return this.pc1.createOffer().then(this.gotOffer.bind(this), console.error.bind(console)); | ||
} | ||
@@ -27,52 +27,54 @@ | ||
// of gathered stats. | ||
gatherStats (peerConnection, statsCb, interval) { | ||
const stats = []; | ||
const statsCollectTime = []; | ||
getStats_(); | ||
gatherStats (peerConnection, interval) { | ||
let stats = []; | ||
let statsCollectTime = []; | ||
function getStats_ () { | ||
if (peerConnection.signalingState === 'closed') { | ||
statsCb(stats, statsCollectTime); | ||
return; | ||
} | ||
// Work around for webrtc/testrtc#74 | ||
if (typeof mozRTCPeerConnection !== 'undefined' && peerConnection instanceof mozRTCPeerConnection) { | ||
setTimeout(getStats_, interval); | ||
} else { | ||
setTimeout(peerConnection.getStats.bind(peerConnection, gotStats_), interval); | ||
} | ||
} | ||
return new Promise((resolve, reject) => { | ||
const getStats = () => { | ||
if (peerConnection.signalingState === 'closed') { | ||
return resolve({stats, statsCollectTime}); | ||
} | ||
// Work around for webrtc/testrtc#74 | ||
if (typeof mozRTCPeerConnection !== 'undefined' && peerConnection instanceof mozRTCPeerConnection) { | ||
setTimeout(getStats, interval); | ||
} else { | ||
setTimeout(peerConnection.getStats.bind(peerConnection, gotStats), interval); | ||
} | ||
}; | ||
function gotStats_ (response) { | ||
for (let index in response.result()) { | ||
stats.push(response.result()[index]); | ||
statsCollectTime.push(Date.now()); | ||
} | ||
getStats_(); | ||
} | ||
const gotStats = (response) => { | ||
const now = Date.now(); | ||
const results = response.result(); | ||
stats = results; | ||
statsCollectTime = results.map(() => now); | ||
getStats(); | ||
}; | ||
getStats(); | ||
}); | ||
} | ||
gotOffer_ (offer) { | ||
if (this.constrainOfferToRemoveVideoFec_) { | ||
offer.sdp = offer.sdp.replace(/(m=video 1 [^\r]+)(116 117)(\r\n)/g, '$1\r\n'); | ||
offer.sdp = offer.sdp.replace(/a=rtpmap:116 red\/90000\r\n/g, ''); | ||
offer.sdp = offer.sdp.replace(/a=rtpmap:117 ulpfec\/90000\r\n/g, ''); | ||
} | ||
gotOffer (offer) { | ||
// if (this.constrainOfferToRemoveVideoFec) { | ||
// offer.sdp = offer.sdp.replace(/(m=video 1 [^\r]+)(116 117)(\r\n)/g, '$1\r\n'); | ||
// offer.sdp = offer.sdp.replace(/a=rtpmap:116 red\/90000\r\n/g, ''); | ||
// offer.sdp = offer.sdp.replace(/a=rtpmap:117 ulpfec\/90000\r\n/g, ''); | ||
// } | ||
this.pc1.setLocalDescription(offer); | ||
this.pc2.setRemoteDescription(offer); | ||
this.pc2.createAnswer(this.gotAnswer_.bind(this), console.error.bind(console)); | ||
return this.pc2.createAnswer().then(this.gotAnswer.bind(this), console.error.bind(console)); | ||
} | ||
gotAnswer_ (answer) { | ||
if (this.constrainVideoBitrateKbps_) { | ||
answer.sdp = answer.sdp.replace(/a=mid:video\r\n/g, 'a=mid:video\r\nb=AS:' + this.constrainVideoBitrateKbps_ + '\r\n'); | ||
gotAnswer (answer) { | ||
if (this.constrainVideoBitrateKbps) { | ||
answer.sdp = answer.sdp.replace(/a=mid:video\r\n/g, 'a=mid:video\r\nb=AS:' + this.constrainVideoBitrateKbps + '\r\n'); | ||
} | ||
this.pc2.setLocalDescription(answer); | ||
this.pc1.setRemoteDescription(answer); | ||
return this.pc1.setRemoteDescription(answer); | ||
} | ||
onIceCandidate_ (otherPeer, event) { | ||
onIceCandidate (otherPeer, event) { | ||
if (event.candidate) { | ||
var parsed = this.parseCandidate(event.candidate.candidate); | ||
if (this.iceCandidateFilter_(parsed)) { | ||
if (this.iceCandidateFilter(parsed)) { | ||
otherPeer.addIceCandidate(event.candidate); | ||
@@ -95,3 +97,3 @@ } | ||
setIceCandidateFilter (filter) { | ||
this.iceCandidateFilter_ = filter; | ||
this.iceCandidateFilter = filter; | ||
} | ||
@@ -101,3 +103,3 @@ | ||
disableVideoFec () { | ||
this.constrainOfferToRemoveVideoFec_ = true; | ||
this.constrainOfferToRemoveVideoFec = true; | ||
} | ||
@@ -107,3 +109,3 @@ | ||
constrainVideoBitrate (maxVideoBitrateKbps) { | ||
this.constrainVideoBitrateKbps_ = maxVideoBitrateKbps; | ||
this.constrainVideoBitrateKbps = maxVideoBitrateKbps; | ||
} | ||
@@ -120,2 +122,2 @@ | ||
export default WebrtcCall; | ||
export default WebrtcCall; |
/* global WebRTCTroubleshooter */ | ||
var video = true; | ||
var audio = true; | ||
let video = true; | ||
let audio = true; | ||
var iceServers = []; | ||
var testSuite = new WebRTCTroubleshooter.TestSuite(); | ||
let iceServers = []; | ||
const webRTCTroubleshooter = WebRTCTroubleshooter.default; | ||
const testSuite = new webRTCTroubleshooter.TestSuite(); | ||
document.getElementById('run-button').onclick = function startTroubleshooter () { | ||
if (!navigator.mediaDevices) { | ||
video = false; | ||
audio = false; | ||
} | ||
const iceServersEntry = document.getElementById('ice-servers'); | ||
const runButton = document.getElementById('run-button'); | ||
var servers = document.getElementById('ice-servers').value; | ||
if (servers) { | ||
iceServers = JSON.parse(servers); | ||
} | ||
var iceConfig = { | ||
iceServers: iceServers, | ||
iceTransports: 'relay' | ||
}; | ||
var mediaOptions = mediaOptions || { audio: true, video: true }; | ||
runButton.onclick = function startTroubleshooter () { | ||
if (!navigator.mediaDevices) { | ||
video = false; | ||
audio = false; | ||
} | ||
if (audio) { | ||
var audioTest = new WebRTCTroubleshooter.AudioTest(mediaOptions); | ||
const servers = iceServersEntry.value; | ||
if (servers) { | ||
iceServers = JSON.parse(servers); | ||
window.localStorage.setItem('iceServers', servers); | ||
} | ||
const iceConfig = { | ||
iceServers: iceServers, | ||
iceTransports: 'relay' | ||
}; | ||
const mediaOptions = { audio: true, video: true }; | ||
testSuite.addTest(audioTest); | ||
} | ||
if (audio) { | ||
const audioTest = new webRTCTroubleshooter.AudioTest(mediaOptions); | ||
if (video) { | ||
var videoTest = new WebRTCTroubleshooter.VideoTest(mediaOptions); | ||
var advancedCameraTest = new WebRTCTroubleshooter.AdvancedCameraTest(mediaOptions); | ||
var bandwidthTest = new WebRTCTroubleshooter.VideoBandwidthTest({ iceConfig: iceConfig, mediaOptions: mediaOptions}); | ||
testSuite.addTest(audioTest); | ||
} | ||
testSuite.addTest(videoTest); | ||
testSuite.addTest(advancedCameraTest); | ||
testSuite.addTest(bandwidthTest); | ||
} | ||
if (video) { | ||
const videoTest = new webRTCTroubleshooter.VideoTest(mediaOptions); | ||
const advancedCameraTest = new webRTCTroubleshooter.AdvancedCameraTest(mediaOptions); | ||
const bandwidthTest = new webRTCTroubleshooter.VideoBandwidthTest({ iceConfig: iceConfig, mediaOptions: mediaOptions }); | ||
if (window.RTCPeerConnection) { | ||
var connectivityTest = new WebRTCTroubleshooter.ConnectivityTest(iceConfig); | ||
var throughputTest = new WebRTCTroubleshooter.ThroughputTest(iceConfig); | ||
testSuite.addTest(videoTest); | ||
testSuite.addTest(advancedCameraTest); | ||
testSuite.addTest(bandwidthTest); | ||
} | ||
testSuite.addTest(connectivityTest); | ||
testSuite.addTest(throughputTest); | ||
} | ||
if (window.RTCPeerConnection) { | ||
const connectivityTest = new webRTCTroubleshooter.ConnectivityTest(iceConfig); | ||
const throughputTest = new webRTCTroubleshooter.ThroughputTest(iceConfig); | ||
testSuite.runNextTest(function() { | ||
console.log('Finished the tests'); | ||
}); | ||
testSuite.addTest(connectivityTest); | ||
testSuite.addTest(throughputTest); | ||
} | ||
testSuite.start().then(function () { | ||
console.log('Finished the tests'); | ||
}); | ||
}; | ||
function willDestroyElement () { | ||
try { | ||
if (testSuite && testSuite.running) { | ||
testSuite.stopAllTests(); | ||
} | ||
} catch (e) { /* don't care - just want to destroy */ } | ||
const savedIceServers = window.localStorage.getItem('iceServers'); | ||
if (iceServers) { | ||
iceServersEntry.value = savedIceServers; | ||
} |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Uses eval
Supply chain riskPackage uses dynamic code execution (e.g., eval()), which is a dangerous practice. This can prevent the code from running in certain environments and increases the risk that the code may contain exploits or malicious behavior.
Found 1 instance in 1 package
5
25
25
7
1
330423
8370