Initial commit

This commit is contained in:
samuelmaddock 2014-07-12 20:03:59 -04:00
commit 5e1d6fe6d0
97 changed files with 9110 additions and 0 deletions

18
.editorconfig Normal file
View File

@ -0,0 +1,18 @@
# EditorConfig helps developers define and maintain consistent
# coding styles between different editors and IDEs
# editorconfig.org
root = true
[*]
end_of_line = crlf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.lua]
indent_style = tab
indent_size = 4
[*.md]
trim_trailing_whitespace = false

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
# general
*.todo
*.sublime-workspace
# old mediaplayer service code
lua/autorun/mediaplayer/services/_deprecated

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
The MIT License
Copyright © 2014 GMod Media Player authors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

12
README.md Normal file
View File

@ -0,0 +1,12 @@
Media Player
============
Media Player is an addon for Garry's Mod which features several media streaming services able to be played synchronously in multiplayer. It is currently in alpha and will be uploaded to Steam Workshop when it's closer to completion.
### Installation ###
Place the contents of this GitHub repository into a new addon folder within your `garrysmod/addons/` directory. For those unfamiliar with Git, press the `Download ZIP` button in the right-hand sidebar.
### Notes ###
Please do not distribute this content elsewhere such as uploading to Steam Workshop. It will be uploaded when it's finished.

10
addon.txt Normal file
View File

@ -0,0 +1,10 @@
"AddonInfo"
{
"name" "Media Player"
"version" "0.0.1"
"author_name" "Samuel Maddock"
"author_email" "contact@pixeltailgames.com"
"author_url" "http://github.com/pixeltailgames"
"info" "http://github.com/pixeltailgames/gm-mediaplayer"
"override" "0"
}

View File

@ -0,0 +1,15 @@
"UnlitGeneric"
{
"$basetexture" "theater/STATIC"
"$surfaceprop" "glass"
"%keywords" "theater"
"Proxies"
{
"AnimatedTexture"
{
"animatedTextureVar" "$basetexture"
"animatedTextureFrameNumVar" "$frame"
"animatedTextureFrameRate" "16"
}
}
}

Binary file not shown.

1
html/.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
* text=auto

6
html/.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
node_modules
dist
.tmp
.sass-cache
bower_components
test/bower_components

21
html/.jshintrc Normal file
View File

@ -0,0 +1,21 @@
{
"node": true,
"browser": true,
"esnext": true,
"bitwise": true,
"camelcase": true,
"curly": true,
"eqeqeq": true,
"immed": true,
"indent": 4,
"latedef": true,
"newcap": true,
"noarg": true,
"quotmark": "single",
"undef": true,
"unused": true,
"strict": true,
"trailing": true,
"smarttabs": true,
"jquery": true
}

406
html/Gruntfile.js Normal file
View File

@ -0,0 +1,406 @@
// Generated on 2014-06-15 using generator-webapp 0.4.9
'use strict';
// # Globbing
// for performance reasons we're only matching one level down:
// 'test/spec/{,*/}*.js'
// use this if you want to recursively match all subfolders:
// 'test/spec/**/*.js'
module.exports = function (grunt) {
// Load grunt tasks automatically
require('load-grunt-tasks')(grunt);
// Time how long tasks take. Can help when optimizing build times
require('time-grunt')(grunt);
// Configurable paths
var config = {
app: 'app',
dist: 'dist'
};
// Define the configuration for all the tasks
grunt.initConfig({
// Project settings
config: config,
// Watches files for changes and runs tasks based on the changed files
watch: {
bower: {
files: ['bower.json'],
tasks: ['bowerInstall']
},
js: {
files: ['<%= config.app %>/scripts/{,*/}*.js'],
tasks: ['jshint'],
options: {
livereload: true
}
},
jstest: {
files: ['test/spec/{,*/}*.js'],
tasks: ['test:watch']
},
gruntfile: {
files: ['Gruntfile.js']
},
sass: {
files: ['<%= config.app %>/styles/{,*/}*.{scss,sass}'],
tasks: ['sass:server', 'autoprefixer']
},
styles: {
files: ['<%= config.app %>/styles/{,*/}*.css'],
tasks: ['newer:copy:styles', 'autoprefixer']
},
livereload: {
options: {
livereload: '<%= connect.options.livereload %>'
},
files: [
'<%= config.app %>/{,*/}*.html',
'.tmp/styles/{,*/}*.css',
'<%= config.app %>/images/{,*/}*'
]
}
},
// The actual grunt server settings
connect: {
options: {
port: 80,
open: true,
livereload: 35729,
// Change this to '0.0.0.0' to access the server from outside
hostname: 'localhost'
},
livereload: {
options: {
middleware: function(connect) {
return [
connect.static('.tmp'),
connect().use('/bower_components', connect.static('./bower_components')),
connect.static(config.app)
];
}
}
},
test: {
options: {
open: false,
port: 9001,
middleware: function(connect) {
return [
connect.static('.tmp'),
connect.static('test'),
connect().use('/bower_components', connect.static('./bower_components')),
connect.static(config.app)
];
}
}
},
dist: {
options: {
base: '<%= config.dist %>',
livereload: false
}
}
},
// Empties folders to start fresh
clean: {
dist: {
files: [{
dot: true,
src: [
'.tmp',
'<%= config.dist %>/*',
'!<%= config.dist %>/.git*'
]
}]
},
server: '.tmp'
},
// Make sure code styles are up to par and there are no obvious mistakes
jshint: {
options: {
jshintrc: '.jshintrc',
reporter: require('jshint-stylish')
},
all: [
'Gruntfile.js',
'<%= config.app %>/scripts/{,*/}*.js',
'!<%= config.app %>/scripts/vendor/*',
'test/spec/{,*/}*.js'
]
},
// Mocha testing framework configuration options
mocha: {
all: {
options: {
run: true,
urls: ['http://<%= connect.test.options.hostname %>:<%= connect.test.options.port %>/index.html']
}
}
},
// Compiles Sass to CSS and generates necessary files if requested
sass: {
options: {
loadPath: [
'bower_components'
]
},
dist: {
files: [{
expand: true,
cwd: '<%= config.app %>/styles',
src: ['*.scss'],
dest: '.tmp/styles',
ext: '.css'
}]
},
server: {
files: [{
expand: true,
cwd: '<%= config.app %>/styles',
src: ['*.scss'],
dest: '.tmp/styles',
ext: '.css'
}]
}
},
// Add vendor prefixed styles
autoprefixer: {
options: {
browsers: ['last 1 version']
},
dist: {
files: [{
expand: true,
cwd: '.tmp/styles/',
src: '{,*/}*.css',
dest: '.tmp/styles/'
}]
}
},
// Automatically inject Bower components into the HTML file
bowerInstall: {
app: {
src: ['<%= config.app %>/index.html'],
exclude: ['bower_components/bootstrap-sass-official/vendor/assets/javascripts/bootstrap.js']
},
sass: {
src: ['<%= config.app %>/styles/{,*/}*.{scss,sass}']
}
},
// Renames files for browser caching purposes
rev: {
dist: {
files: {
src: [
'<%= config.dist %>/scripts/{,*/}*.js',
'<%= config.dist %>/styles/{,*/}*.css',
'<%= config.dist %>/images/{,*/}*.*',
'<%= config.dist %>/styles/fonts/{,*/}*.*',
'<%= config.dist %>/*.{ico,png}'
]
}
}
},
// Reads HTML for usemin blocks to enable smart builds that automatically
// concat, minify and revision files. Creates configurations in memory so
// additional tasks can operate on them
useminPrepare: {
options: {
dest: '<%= config.dist %>'
},
html: '<%= config.app %>/index.html'
},
// Performs rewrites based on rev and the useminPrepare configuration
usemin: {
options: {
assetsDirs: ['<%= config.dist %>', '<%= config.dist %>/images']
},
html: ['<%= config.dist %>/{,*/}*.html'],
css: ['<%= config.dist %>/styles/{,*/}*.css']
},
// The following *-min tasks produce minified files in the dist folder
imagemin: {
dist: {
files: [{
expand: true,
cwd: '<%= config.app %>/images',
src: '{,*/}*.{gif,jpeg,jpg,png}',
dest: '<%= config.dist %>/images'
}]
}
},
svgmin: {
dist: {
files: [{
expand: true,
cwd: '<%= config.app %>/images',
src: '{,*/}*.svg',
dest: '<%= config.dist %>/images'
}]
}
},
htmlmin: {
dist: {
options: {
collapseBooleanAttributes: true,
collapseWhitespace: true,
removeAttributeQuotes: true,
removeCommentsFromCDATA: true,
removeEmptyAttributes: true,
removeOptionalTags: true,
removeRedundantAttributes: true,
useShortDoctype: true
},
files: [{
expand: true,
cwd: '<%= config.dist %>',
src: '{,*/}*.html',
dest: '<%= config.dist %>'
}]
}
},
// By default, your `index.html`'s <!-- Usemin block --> will take care of
// minification. These next options are pre-configured if you do not wish
// to use the Usemin blocks.
// cssmin: {
// dist: {
// files: {
// '<%= config.dist %>/styles/main.css': [
// '.tmp/styles/{,*/}*.css',
// '<%= config.app %>/styles/{,*/}*.css'
// ]
// }
// }
// },
// uglify: {
// dist: {
// files: {
// '<%= config.dist %>/scripts/scripts.js': [
// '<%= config.dist %>/scripts/scripts.js'
// ]
// }
// }
// },
// concat: {
// dist: {}
// },
// Copies remaining files to places other tasks can use
copy: {
dist: {
files: [{
expand: true,
dot: true,
cwd: '<%= config.app %>',
dest: '<%= config.dist %>',
src: [
'*.{ico,png,txt}',
'.htaccess',
'images/{,*/}*.webp',
'{,*/}*.html',
'styles/fonts/{,*/}*.*'
]
}]
},
styles: {
expand: true,
dot: true,
cwd: '<%= config.app %>/styles',
dest: '.tmp/styles/',
src: '{,*/}*.css'
}
},
// Run some tasks in parallel to speed up build process
concurrent: {
server: [
'sass:server',
'copy:styles'
],
test: [
'copy:styles'
],
dist: [
'sass',
'copy:styles',
'imagemin',
'svgmin'
]
}
});
grunt.registerTask('serve', function (target) {
if (target === 'dist') {
return grunt.task.run(['build', 'connect:dist:keepalive']);
}
grunt.task.run([
'clean:server',
'concurrent:server',
'autoprefixer',
'connect:livereload',
'watch'
]);
});
grunt.registerTask('server', function (target) {
grunt.log.warn('The `server` task has been deprecated. Use `grunt serve` to start a server.');
grunt.task.run([target ? ('serve:' + target) : 'serve']);
});
grunt.registerTask('test', function (target) {
if (target !== 'watch') {
grunt.task.run([
'clean:server',
'concurrent:test',
'autoprefixer'
]);
}
grunt.task.run([
'connect:test',
'mocha'
]);
});
grunt.registerTask('build', [
'clean:dist',
'useminPrepare',
'concurrent:dist',
'autoprefixer',
'concat',
'cssmin',
'uglify',
'copy:dist',
'rev',
'usemin',
'htmlmin'
]);
grunt.registerTask('default', [
'newer:jshint',
'test',
'build'
]);
};

BIN
html/app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

24
html/app/index.html Normal file
View File

@ -0,0 +1,24 @@
<!doctype html>
<html class="no-js">
<head>
<meta charset="utf-8">
<title>Garry's Mod MediaPlayer</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width">
<link rel="shortcut icon" href="/favicon.ico">
<!-- Place favicon.ico and apple-touch-icon.png in the root directory -->
<!-- build:css styles/vendor.css -->
<!-- bower:css -->
<!-- endbower -->
<!-- endbuild -->
<!-- build:css(.tmp) styles/main.css -->
<link rel="stylesheet" href="styles/main.css">
<!-- endbuild -->
</head>
<body>
<!-- build:js({app,.tmp}) scripts/main.js -->
<script src="scripts/main.js"></script>
<!-- endbuild -->
</body>
</html>

16
html/app/scripts/main.js Normal file
View File

@ -0,0 +1,16 @@
'use strict';
window.MP = (function () {
var elem = document.body;
return {
setHtml: function (html) {
elem.innerHTML = html;
console.log(elem.innerHTML);
}
};
}());

17
html/app/styles/main.scss Normal file
View File

@ -0,0 +1,17 @@
// bower:scss
// endbower
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
}
* { box-sizing: border-box }
body {
background-color: #ececec;
color: #313131;
overflow: hidden;
}

95
html/app/vimeo.html Normal file
View File

@ -0,0 +1,95 @@
<!doctype html>
<html class="no-js">
<head>
<meta charset="utf-8">
<title>Garry's Mod MediaPlayer</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width">
<link rel="shortcut icon" href="/favicon.ico">
<!-- Place favicon.ico and apple-touch-icon.png in the root directory -->
<!-- build:css styles/vendor.css -->
<!-- bower:css -->
<!-- endbower -->
<!-- endbuild -->
<!-- build:css(.tmp) styles/main.css -->
<link rel="stylesheet" href="styles/main.css">
<!-- endbuild -->
</head>
<body>
<iframe id="player_1" width="100%" height="100%" frameborder="0"></iframe>
<script src="http://a.vimeocdn.com/js/froogaloop2.min.js"></script>
<script type="text/javascript">
console.log('Running script');
window.MediaPlayer = (function(){
var videoId = location.hash.substr(1);
if (!videoId.length) return;
var url = 'http://player.vimeo.com/video/',
params = '?api=1&player_id=player_1&autoplay=true';
console.info('Loading Vimeo player...');
var vimeoPlayers = document.querySelectorAll('iframe'),
player,
froogaloop,
curVolume,
lastSeekTime,
seekThreshold = 5; // seconds
for (var i = 0, length = vimeoPlayers.length; i < length; i++) {
player = vimeoPlayers[i];
player.src = url + videoId + params;
$f(player).addEvent('ready', ready);
}
function ready (playerId) {
froogaloop = $f(playerId);
console.info('Vimeo player is ready!');
}
return {
setVolume: function (volume) {
if (!froogaloop) return;
if (curVolume !== volume) {
froogaloop.api('setVolume', volume);
curVolume = volume;
}
},
seek: function (seekTime) {
if (!froogaloop) return;
var curTime = (new Date).getTime();
if (lastSeekTime && (((curTime - lastSeekTime) / 1000) < seekThreshold)) {
return;
}
froogaloop.api('paused', function (paused) {
if (paused) return;
froogaloop.api('getDuration', function (duration) {
if (seekTime > duration) return;
froogaloop.api('getCurrentTime', function (curTime) {
var diffTime = Math.abs(curTime - seekTime);
if (diffTime < seekThreshold) return;
froogaloop.api('seekTo', seekTime);
lastSeekTime = (new Date).getTime();
});
});
});
}
};
})();
</script>
</body>
</html>

View File

@ -0,0 +1,642 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="width=700,maximum-scale=1.0,user-scalable=yes">
<title>Vimeo Player API Playground</title>
<link rel="stylesheet" href="http://a.vimeocdn.com/p/1.4.28/css/api_examples.css">
<style>
body, html, dl, dd { margin: 0; padding: 0;}
body, html {
font-family: "Helvetica Neue", Helvetica, Arial, Geneva, sans-serif;
color: #343434;
background: #f7f7f7;
}
iframe {
display:block;
border:0;
margin-bottom: 20px;
}
h1 {
font-weight: normal;
text-align: center;
}
button {
background: #e9e9e9;
cursor: pointer;
border: 1px solid #ddd;
padding: 2px 13px;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
margin: 0;
cursor: pointer;
}
.wrapper {
width: 600px;
margin: 0 auto;
margin-bottom: 30px;
}
.wrapper .container > div {
background: #fff;
padding: 30px;
border: 1px solid #ddd;
box-sizing: border-box;
margin-top: 30px;
}
.wrapper .container:first-child {
margin-top: 0;
}
.wrapper .container h2 {
margin: 0 0 20px 0;
}
.wrapper .container button {
line-height: 19px;
}
.wrapper .container input[type="text"] {
font-size: 12px;
}
button input[type="text"] {
border: 1px solid #ddd;
text-align: center;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
margin: 0 -11px 0 5px;
padding: 2px;
}
.wrapper div dl {
margin-bottom: 15px;
}
.wrapper div dt {
margin-bottom: 5px;
}
.wrapper div dt span {
font-size: 0.9em;
color: #666;
}
.wrapper div dd {
display: inline-block;
font-size: 12px;
margin-bottom: 5px;
}
.wrapper div dd label {
margin-right: 10px;
}
.wrapper div .output {
display: block;
width: 100%;
height: 100px;
border: 1px solid #ddd;
overflow-y: auto;
font-size: 13px;
padding: 5px;
box-sizing: border-box;
line-height: 1.4em;
resize: vertical;
font-family: Inconsolata, Monaco, monospace;
}
.wrapper .container > button {
border: 1px solid #ddd;
font-weight: bold;
font-size: 1.2em;
margin: -1px 0 0 0;
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.wrapper > button input[type="text"] {
padding: 6px;
}
.console dd {
width: 100%;
white-space: nowrap;
}
.console .clear {
margin-top: 10px;
}
</style>
</head>
<body>
<div class="wrapper">
<hgroup>
<h1><img src="http://a.vimeocdn.com/images_v6/vimeo_logo.png"> Player API Playground</h1>
</hgroup>
<div class="container">
<div>
<h2>Vimeo Player 1</h2>
<iframe id="player_1" src="http://player.vimeo.com/video/76979871?api=1&amp;player_id=player_1" width="540" height="304" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>
<dl class="simple">
<dt>Simple Buttons <span>(buttons with simple API calls)</span></dt>
<dd><button class="play">Play</button></dd>
<dd><button class="pause">Pause</button></dd>
<dd><button class="unload">Unload</button></dd>
</dl>
<dl class="modifiers">
<dt>Modifier Buttons <span>(buttons that modify the player)</span></dt>
<dd><button class="seek">Seek <input type="text" value="30" size="3" maxlength="3" /></button></dd>
<dd><button class="volume">Volume <input type="text" value="0.5" size="3" maxlength="3" /></button></dd>
<dd><button class="loop">Loop <input type="text" value="0" size="3" maxlength="3" /></button></dd>
<dd><button class="color">Color <input type="text" value="00adef" size="6" /></button></dd>
<dd><button class="randomColor">Random Color</button></dd>
<dd><button class="load">Load Video <input type="text" value="2" size="10" /></button></dd>
</dl>
<dl class="getters">
<dt>Getters <span>(buttons that get back a value; logged out in the console)</span></dt>
<dd><button class="time">Current Time</button></dd>
<dd><button class="duration">Duration</button></dd>
<dd><button class="color">Color</button></dd>
<dd><button class="url">URL</button></dd>
<dd><button class="embed">Embed Code</button></dd>
<dd><button class="paused">Paused</button></dd>
<dd><button class="getVolume">Volume</button></dd>
<dd><button class="height">Video Height</button></dd>
<dd><button class="width">Video Width</button></dd>
</dl>
<dl class="listeners">
<dt>Event Listeners</dt>
<dd><label><input class="loadProgress" type="checkbox" checked />loadProgress</label></dd>
<dd><label><input class="playProgress" type="checkbox" checked />playProgress</label></dd>
<dd><label><input class="play" type="checkbox" checked />play</label></dd>
<dd><label><input class="pause" type="checkbox" checked />pause</label></dd>
<dd><label><input class="finish" type="checkbox" checked />finish</label></dd>
<dd><label><input class="seek" type="checkbox" checked />seek</label></dd>
<dd><label><input class="cuechange" type="checkbox" checked />cuechange</label></dd>
</dl>
<dl class="console">
<dt>Console</dt>
<dd><textarea class="output" readonly="readonly"></textarea><button class="clear">Clear</button></dd>
</dl>
</div>
<button class="addClip" title="Add a new clip.">+ <input type="text" value="3718294" size="8" /></button>
</div>
<script src="http://a.vimeocdn.com/js/froogaloop2.min.js?97273-1352487961"></script>
<script>
(function(){
// Listen for the ready event for any vimeo video players on the page
var vimeoPlayers = document.querySelectorAll('iframe'),
player;
for (var i = 0, length = vimeoPlayers.length; i < length; i++) {
player = vimeoPlayers[i];
$f(player).addEvent('ready', ready);
}
/**
* Utility function for adding an event. Handles the inconsistencies
* between the W3C method for adding events (addEventListener) and
* IE's (attachEvent).
*/
function addEvent(element, eventName, callback) {
if (element.addEventListener) {
element.addEventListener(eventName, callback, false);
}
else {
element.attachEvent('on' + eventName, callback);
}
}
/**
* Called once a vimeo player is loaded and ready to receive
* commands. You can add events and make api calls only after this
* function has been called.
*/
function ready(player_id) {
// Keep a reference to Froogaloop for this player
var container = document.getElementById(player_id).parentElement.parentElement,
froogaloop = $f(player_id),
apiConsole = container.querySelector('.console .output');
/**
* Prepends log messages to the example console for you to see.
*/
function apiLog(message) {
apiConsole.innerHTML = message + '\n' + apiConsole.innerHTML;
}
/**
* Sets up the actions for the buttons that will perform simple
* api calls to Froogaloop (play, pause, etc.). These api methods
* are actions performed on the player that take no parameters and
* return no values.
*/
function setupSimpleButtons() {
var buttons = container.querySelector('div dl.simple'),
playBtn = buttons.querySelector('.play'),
pauseBtn = buttons.querySelector('.pause'),
unloadBtn = buttons.querySelector('.unload');
// Call play when play button clicked
addEvent(playBtn, 'click', function() {
froogaloop.api('play');
}, false);
// Call pause when pause button clicked
addEvent(pauseBtn, 'click', function() {
froogaloop.api('pause');
}, false);
// Call unload when unload button clicked
addEvent(unloadBtn, 'click', function() {
froogaloop.api('unload');
}, false);
}
/**
* Sets up the actions for the buttons that will modify certain
* things about the player and the video in it. These methods
* take a parameter, such as a color value when setting a color.
*/
function setupModifierButtons() {
var buttons = container.querySelector('div dl.modifiers'),
seekBtn = buttons.querySelector('.seek'),
volumeBtn = buttons.querySelector('.volume'),
loopBtn = buttons.querySelector('.loop'),
colorBtn = buttons.querySelector('.color'),
randomColorBtn = buttons.querySelector('.randomColor'),
loadVideoBtn = buttons.querySelector('.load');
// Call seekTo when seek button clicked
addEvent(seekBtn, 'click', function(e) {
// Don't do anything if clicking on anything but the button (such as the input field)
if (e.target != this) {
return false;
}
// Grab the value in the input field
var seekVal = this.querySelector('input').value;
// Call the api via froogaloop
froogaloop.api('seekTo', seekVal);
}, false);
// Call setVolume when volume button clicked
addEvent(volumeBtn, 'click', function(e) {
// Don't do anything if clicking on anything but the button (such as the input field)
if (e.target != this) {
return false;
}
// Grab the value in the input field
var volumeVal = this.querySelector('input').value;
// Call the api via froogaloop
froogaloop.api('setVolume', volumeVal);
}, false);
// Call setLoop when loop button clicked
addEvent(loopBtn, 'click', function(e) {
// Don't do anything if clicking on anything but the button (such as the input field)
if (e.target != this) {
return false;
}
// Grab the value in the input field
var loopVal = this.querySelector('input').value;
//Call the api via froogaloop
froogaloop.api('setLoop', loopVal);
}, false);
// Call setColor when color button clicked
addEvent(colorBtn, 'click', function(e) {
// Don't do anything if clicking on anything but the button (such as the input field)
if (e.target != this) {
return false;
}
// Grab the value in the input field
var colorVal = this.querySelector('input').value;
// Call the api via froogaloop
froogaloop.api('setColor', colorVal);
}, false);
// Call setColor with a random color when random color button clicked
addEvent(randomColorBtn, 'click', function(e) {
// Don't do anything if clicking on anything but the button (such as the input field)
if (e.target != this) {
return false;
}
// Generate a random color
var colorVal = Math.floor(Math.random() * 16777215).toString(16);
// Call the api via froogaloop
froogaloop.api('setColor', colorVal);
}, false);
addEvent(loadVideoBtn, 'click', function(e) {
// Don't do anything if clicking on anything but the button (such as the input field)
if (e.target != this) {
return false;
}
var videoVal = this.querySelector('input').value;
// Call the api via froogaloop
froogaloop.api('loadVideo', videoVal);
}, false);
}
/**
* Sets up actions for buttons that will ask the player for something,
* such as the current time or duration. These methods require a
* callback function which will be called with any data as the first
* parameter in that function.
*/
function setupGetterButtons() {
var buttons = container.querySelector('div dl.getters'),
timeBtn = buttons.querySelector('.time'),
durationBtn = buttons.querySelector('.duration'),
colorBtn = buttons.querySelector('.color'),
urlBtn = buttons.querySelector('.url'),
embedBtn = buttons.querySelector('.embed'),
pausedBtn = buttons.querySelector('.paused'),
getVolumeBtn = buttons.querySelector('.getVolume'),
widthBtn = buttons.querySelector('.width'),
heightBtn = buttons.querySelector('.height');
// Get the current time and log it to the API console when time button clicked
addEvent(timeBtn, 'click', function(e) {
froogaloop.api('getCurrentTime', function (value, player_id) {
// Log out the value in the API Console
apiLog('getCurrentTime : ' + value);
});
}, false);
// Get the duration and log it to the API console when time button clicked
addEvent(durationBtn, 'click', function(e) {
froogaloop.api('getDuration', function (value, player_id) {
// Log out the value in the API Console
apiLog('getDuration : ' + value);
});
}, false);
// Get the embed color and log it to the API console when time button clicked
addEvent(colorBtn, 'click', function(e) {
froogaloop.api('getColor', function (value, player_id) {
// Log out the value in the API Console
apiLog('getColor : ' + value);
});
}, false);
// Get the video url and log it to the API console when time button clicked
addEvent(urlBtn, 'click', function(e) {
froogaloop.api('getVideoUrl', function (value, player_id) {
// Log out the value in the API Console
apiLog('getVideoUrl : ' + value);
});
}, false);
// Get the embed code and log it to the API console when time button clicked
addEvent(embedBtn, 'click', function(e) {
froogaloop.api('getVideoEmbedCode', function (value, player_id) {
// Use html entities for less-than and greater-than signs
value = value.replace(/</g, '&lt;').replace(/>/g, '&gt;');
// Log out the value in the API Console
apiLog('getVideoEmbedCode : ' + value);
});
}, false);
// Get the paused state and log it to the API console when time button clicked
addEvent(pausedBtn, 'click', function(e) {
froogaloop.api('paused', function (value, player_id) {
// Log out the value in the API Console
apiLog('paused : ' + value);
});
}, false);
// Get the paused state and log it to the API console when time button clicked
addEvent(getVolumeBtn, 'click', function(e) {
froogaloop.api('getVolume', function (value, player_id) {
// Log out the value in the API Console
apiLog('volume : ' + value);
});
}, false);
// Get the paused state and log it to the API console when time button clicked
addEvent(widthBtn, 'click', function(e) {
froogaloop.api('getVideoWidth', function (value, player_id) {
// Log out the value in the API Console
apiLog('getVideoWidth : ' + value);
});
}, false);
// Get the paused state and log it to the API console when time button clicked
addEvent(heightBtn, 'click', function(e) {
froogaloop.api('getVideoHeight', function (value, player_id) {
// Log out the value in the API Console
apiLog('getVideoHeight : ' + value);
});
}, false);
}
/**
* Adds listeners for the events that are checked. Adding an event
* through Froogaloop requires the event name and the callback method
* that is called once the event fires.
*/
function setupEventListeners() {
var checkboxes = container.querySelector('.listeners'),
loadProgressChk = checkboxes.querySelector('.loadProgress'),
playProgressChk = checkboxes.querySelector('.playProgress'),
playChk = checkboxes.querySelector('.play'),
pauseChk = checkboxes.querySelector('.pause'),
finishChk = checkboxes.querySelector('.finish'),
seekChk = checkboxes.querySelector('.seek'),
cuechangeChk = checkboxes.querySelector('.cuechange');
function onLoadProgress() {
if (loadProgressChk.checked) {
froogaloop.addEvent('loadProgress', function(data) {
apiLog('loadProgress event : ' + data.percent + ' : ' + data.bytesLoaded + ' : ' + data.bytesTotal + ' : ' + data.duration);
});
}
else {
froogaloop.removeEvent('loadProgress');
}
}
function onPlayProgress() {
if (playProgressChk.checked) {
froogaloop.addEvent('playProgress', function(data) {
apiLog('playProgress event : ' + data.seconds + ' : ' + data.percent + ' : ' + data.duration);
});
}
else {
froogaloop.removeEvent('playProgress');
}
}
function onPlay() {
if (playChk.checked) {
froogaloop.addEvent('play', function(data) {
apiLog('play event');
});
}
else {
froogaloop.removeEvent('play');
}
}
function onPause() {
if (pauseChk.checked) {
froogaloop.addEvent('pause', function(data) {
apiLog('pause event');
});
}
else {
froogaloop.removeEvent('pause');
}
}
function onFinish() {
if (finishChk.checked) {
froogaloop.addEvent('finish', function(data) {
apiLog('finish');
});
}
else {
froogaloop.removeEvent('finish');
}
}
function onSeek() {
if (seekChk.checked) {
froogaloop.addEvent('seek', function(data) {
apiLog('seek event : ' + data.seconds + ' : ' + data.percent + ' : ' + data.duration);
});
}
else {
froogaloop.removeEvent('seek');
}
}
function onCueChange() {
if (cuechangeChk.checked) {
froogaloop.addEvent('cuechange', function(data) {
apiLog('cuechange event : ' + data.language + ' : ' + data.text);
});
}
else {
froogaloop.removeEvent('cuechange');
}
}
// Listens for the checkboxes to change
addEvent(loadProgressChk, 'change', onLoadProgress, false);
addEvent(playProgressChk, 'change', onPlayProgress, false);
addEvent(playChk, 'change', onPlay, false);
addEvent(pauseChk, 'change', onPause, false);
addEvent(finishChk, 'change', onFinish, false);
addEvent(seekChk, 'change', onSeek, false);
addEvent(cuechangeChk, 'change', onCueChange, false);
// Calls the change event if the option is checked
// (this makes sure the checked events get attached on page load as well as on changed)
onLoadProgress();
onPlayProgress();
onPlay();
onPause();
onFinish();
onSeek();
onCueChange();
}
/**
* Sets up actions for adding a new clip window to the page.
*/
function setupAddClip() {
var button = container.querySelector('.addClip'),
newContainer;
addEvent(button, 'click', function(e) {
// Don't do anything if clicking on anything but the button (such as the input field)
if (e.target != this) {
return false;
}
// Gets the index of the current player by simply grabbing the number after the underscore
var currentIndex = parseInt(player_id.split('_')[1]),
clipId = button.querySelector('input').value;
newContainer = resetContainer(container.cloneNode(true), currentIndex+1, clipId);
container.parentNode.appendChild(newContainer);
$f(newContainer.querySelector('iframe')).addEvent('ready', ready);
});
/**
* Resets the duplicate container's information, clearing out anything
* that doesn't pertain to the new clip. It also sets the iframe to
* use the new clip's id as its url.
*/
function resetContainer(element, index, clipId) {
var newHeading = element.querySelector('h2'),
newIframe = element.querySelector('iframe'),
newCheckBoxes = element.querySelectorAll('.listeners input[type="checkbox"]'),
newApiConsole = element.querySelector('.console .output'),
newAddBtn = element.querySelector('.addClip');
// Set the heading text
newHeading.innerText = 'Vimeo Player ' + index;
// Set the correct source of the new clip id
newIframe.src = 'http://player.vimeo.com/video/' + clipId + '?api=1&player_id=player_' + index;
newIframe.id = 'player_' + index;
// Reset all the checkboxes for listeners to be checked on
for (var i = 0, length = newCheckBoxes.length, checkbox; i < length; i++) {
checkbox = newCheckBoxes[i];
checkbox.setAttribute('checked', 'checked');
}
// Clear out the API console
newApiConsole.innerHTML = '';
// Update the clip ID of the add clip button
newAddBtn.querySelector('input').setAttribute('value', clipId);
return element;
}
}
setupSimpleButtons();
setupModifierButtons();
setupGetterButtons();
setupEventListeners();
setupAddClip();
// Setup clear console button
var clearBtn = container.querySelector('.console button');
addEvent(clearBtn, 'click', function(e) {
apiConsole.innerHTML = '';
}, false);
apiLog(player_id + ' ready!');
}
})();
</script>
</body>
</html>

8
html/bower.json Normal file
View File

@ -0,0 +1,8 @@
{
"name": "html",
"private": true,
"dependencies": {
"jquery": "~1.11.0"
},
"devDependencies": {}
}

33
html/package.json Normal file
View File

@ -0,0 +1,33 @@
{
"name": "GMod MediaPlayer",
"version": "0.0.0",
"dependencies": {},
"devDependencies": {
"grunt": "~0.4.1",
"grunt-contrib-copy": "~0.5.0",
"grunt-contrib-concat": "~0.3.0",
"grunt-contrib-uglify": "~0.4.0",
"grunt-contrib-sass": "~0.7.3",
"grunt-contrib-jshint": "~0.9.2",
"grunt-contrib-cssmin": "~0.9.0",
"grunt-contrib-connect": "~0.7.1",
"grunt-contrib-clean": "~0.5.0",
"grunt-contrib-htmlmin": "~0.2.0",
"grunt-bower-install": "~1.4.0",
"grunt-contrib-imagemin": "~0.6.0",
"grunt-contrib-watch": "~0.6.1",
"grunt-rev": "~0.1.0",
"grunt-autoprefixer": "~0.7.2",
"grunt-usemin": "~2.1.0",
"grunt-mocha": "~0.4.10",
"grunt-newer": "~0.7.0",
"grunt-svgmin": "~0.4.0",
"grunt-concurrent": "~0.5.0",
"load-grunt-tasks": "~0.4.0",
"time-grunt": "~0.3.1",
"jshint-stylish": "~0.1.5"
},
"engines": {
"node": ">=0.10.0"
}
}

3
html/test/.bowerrc Normal file
View File

@ -0,0 +1,3 @@
{
"directory": "bower_components"
}

9
html/test/bower.json Normal file
View File

@ -0,0 +1,9 @@
{
"name": "html",
"private": true,
"dependencies": {
"chai": "~1.8.0",
"mocha": "~1.14.0"
},
"devDependencies": {}
}

27
html/test/index.html Normal file
View File

@ -0,0 +1,27 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Mocha Spec Runner</title>
<link rel="stylesheet" href="bower_components/mocha/mocha.css">
</head>
<body>
<div id="mocha"></div>
<script src="bower_components/mocha/mocha.js"></script>
<script>mocha.setup('bdd')</script>
<script src="bower_components/chai/chai.js"></script>
<script>
var assert = chai.assert;
var expect = chai.expect;
var should = chai.should();
</script>
<!-- include source files here... -->
<!-- include spec files here... -->
<script src="spec/test.js"></script>
<script>mocha.run()</script>
</body>
</html>

13
html/test/spec/test.js Normal file
View File

@ -0,0 +1,13 @@
/* global describe, it */
(function () {
'use strict';
describe('Give it some context', function () {
describe('maybe a bit more context here', function () {
it('should run here few assertions', function () {
});
});
});
})();

View File

@ -0,0 +1,76 @@
local CurTime = CurTime
local RealTime = RealTime
local FrameTime = FrameTime
local Material = Material
local pairs = pairs
local tonumber = tonumber
local table = table
local string = string
local type = type
local surface = surface
local HSVToColor = HSVToColor
local Lerp = Lerp
local Msg = Msg
local math = math
local draw = draw
local cam = cam
local Matrix = Matrix
local Angle = Angle
local Vector = Vector
local setmetatable = setmetatable
local ScrW = ScrW
local ScrH = ScrH
local ValidPanel = ValidPanel
local Color = Color
local color_white = color_white
local color_black = color_black
local TEXT_ALIGN_LEFT = TEXT_ALIGN_LEFT
local TEXT_ALIGN_RIGHT = TEXT_ALIGN_RIGHT
local TEXT_ALIGN_TOP = TEXT_ALIGN_TOP
local TEXT_ALIGN_BOTTOM = TEXT_ALIGN_BOTTOM
local TEXT_ALIGN_CENTER = TEXT_ALIGN_CENTER
function draw.HTMLPanel( panel, w, h )
if not ValidPanel( panel ) then return end
if not (w and h) then return end
panel:UpdateHTMLTexture()
local pw, ph = panel:GetSize()
-- Convert to scalar
w = w / pw
h = h / ph
-- Fix for non-power-of-two html panel size
pw = math.CeilPower2(pw)
ph = math.CeilPower2(ph)
surface.SetDrawColor( 255, 255, 255, 255 )
surface.SetMaterial( panel:GetHTMLMaterial() )
surface.DrawTexturedRect( 0, 0, w * pw, h * ph )
end
draw.HTMLTexture = draw.HTMLPanel
function draw.HTMLMaterial( mat, w, h )
if not (w and h) then return end
local pw, ph = w, h
-- Convert to scalar
w = w / pw
h = h / ph
-- Fix for non-power-of-two html panel size
pw = math.CeilPower2(pw)
ph = math.CeilPower2(ph)
surface.SetDrawColor( 255, 255, 255, 255 )
surface.SetMaterial( mat )
surface.DrawTexturedRect( 0, 0, w * pw, h * ph )
end

View File

@ -0,0 +1,10 @@
local file = file
function file.ReadJSON( name, path )
path = path or "DATA"
local json = file.Read( name, path )
if not json then return end
return util.JSONToTable(json)
end

View File

@ -0,0 +1,9 @@
local math = math
local ceil = math.ceil
local log = math.log
local pow = math.pow
-- Ceil the given number to the largest power of two
function math.CeilPower2(n)
return pow(2, ceil(log(n) / log(2)))
end

View File

@ -0,0 +1,21 @@
---
-- Method for easily grabbing a value from a table without checking that each
-- fragment exists.
--
-- @param tbl Table
-- @param key e.g. "json.key.fragments"
--
function table.Lookup( tbl, key )
local fragments = string.Split(key, '.')
local value = tbl
for _, fragment in ipairs(fragments) do
value = value[fragment]
if not value then
return nil
end
end
return value
end

View File

@ -0,0 +1,452 @@
-----------------------------------------------------------------------------
-- URI parsing, composition and relative URL resolution
-- LuaSocket toolkit.
-- Author: Diego Nehab
-- RCS ID: $Id: url.lua,v 1.38 2006/04/03 04:45:42 diego Exp $
-----------------------------------------------------------------------------
-----------------------------------------------------------------------------
-- Declare module
-----------------------------------------------------------------------------
local string = string
local base = _G
local table = table
local pairs = pairs
local ipairs = ipairs
local tonumber = tonumber
local type = type
module("url")
-----------------------------------------------------------------------------
-- Module version
-----------------------------------------------------------------------------
_VERSION = "URL 1.0.1"
-----------------------------------------------------------------------------
-- HTML Entity Translation Table
-- http://lua-users.org/lists/lua-l/2005-10/msg00328.html
-----------------------------------------------------------------------------
local entities = {
[' '] = '&nbsp;',
['¡'] = '&iexcl;',
['¢'] = '&cent;',
['£'] = '&pound;',
['¤'] = '&curren;',
['¥'] = '&yen;',
['¦'] = '&brvbar;',
['§'] = '&sect;',
['¨'] = '&uml;',
['©'] = '&copy;',
['ª'] = '&ordf;',
['«'] = '&laquo;',
['¬'] = '&not;',
['­'] = '&shy;',
['®'] = '&reg;',
['¯'] = '&macr;',
['°'] = '&deg;',
['±'] = '&plusmn;',
['²'] = '&sup2;',
['³'] = '&sup3;',
['´'] = '&acute;',
['µ'] = '&micro;',
[''] = '&para;',
['·'] = '&middot;',
['¸'] = '&cedil;',
['¹'] = '&sup1;',
['º'] = '&ordm;',
['»'] = '&raquo;',
['¼'] = '&frac14;',
['½'] = '&frac12;',
['¾'] = '&frac34;',
['¿'] = '&iquest;',
['À'] = '&Agrave;',
['Á'] = '&Aacute;',
['Â'] = '&Acirc;',
['Ã'] = '&Atilde;',
['Ä'] = '&Auml;',
['Å'] = '&Aring;',
['Æ'] = '&AElig;',
['Ç'] = '&Ccedil;',
['È'] = '&Egrave;',
['É'] = '&Eacute;',
['Ê'] = '&Ecirc;',
['Ë'] = '&Euml;',
['Ì'] = '&Igrave;',
['Í'] = '&Iacute;',
['Î'] = '&Icirc;',
['Ï'] = '&Iuml;',
['Ð'] = '&ETH;',
['Ñ'] = '&Ntilde;',
['Ò'] = '&Ograve;',
['Ó'] = '&Oacute;',
['Ô'] = '&Ocirc;',
['Õ'] = '&Otilde;',
['Ö'] = '&Ouml;',
['×'] = '&times;',
['Ø'] = '&Oslash;',
['Ù'] = '&Ugrave;',
['Ú'] = '&Uacute;',
['Û'] = '&Ucirc;',
['Ü'] = '&Uuml;',
['Ý'] = '&Yacute;',
['Þ'] = '&THORN;',
['ß'] = '&szlig;',
['à'] = '&agrave;',
['á'] = '&aacute;',
['â'] = '&acirc;',
['ã'] = '&atilde;',
['ä'] = '&auml;',
['å'] = '&aring;',
['æ'] = '&aelig;',
['ç'] = '&ccedil;',
['è'] = '&egrave;',
['é'] = '&eacute;',
['ê'] = '&ecirc;',
['ë'] = '&euml;',
['ì'] = '&igrave;',
['í'] = '&iacute;',
['î'] = '&icirc;',
['ï'] = '&iuml;',
['ð'] = '&eth;',
['ñ'] = '&ntilde;',
['ò'] = '&ograve;',
['ó'] = '&oacute;',
['ô'] = '&ocirc;',
['õ'] = '&otilde;',
['ö'] = '&ouml;',
['÷'] = '&divide;',
['ø'] = '&oslash;',
['ù'] = '&ugrave;',
['ú'] = '&uacute;',
['û'] = '&ucirc;',
['ü'] = '&uuml;',
['ý'] = '&yacute;',
['þ'] = '&thorn;',
['ÿ'] = '&yuml;',
['"'] = '&quot;',
["'"] = '&#39;',
['<'] = '&lt;',
['>'] = '&gt;',
['&'] = '&amp;'
}
function htmlentities(s)
for k, v in pairs(entities) do
s = string.gsub(s, k, v)
end
return s
end
function htmlentities_decode(s)
for k, v in pairs(entities) do
s = string.gsub(s, v, k)
end
return s
end
-----------------------------------------------------------------------------
-- Encodes a string into its escaped hexadecimal representation
-- Input
-- s: binary string to be encoded
-- Returns
-- escaped representation of string binary
-----------------------------------------------------------------------------
function escape(s)
return string.gsub(s, "([^A-Za-z0-9_])", function(c)
return string.format("%%%02x", string.byte(c))
end)
end
-----------------------------------------------------------------------------
-- Protects a path segment, to prevent it from interfering with the
-- url parsing.
-- Input
-- s: binary string to be encoded
-- Returns
-- escaped representation of string binary
-----------------------------------------------------------------------------
local function make_set(t)
local s = {}
for i,v in ipairs(t) do
s[t[i]] = 1
end
return s
end
-- these are allowed withing a path segment, along with alphanum
-- other characters must be escaped
local segment_set = make_set {
"-", "_", ".", "!", "~", "*", "'", "(",
")", ":", "@", "&", "=", "+", "$", ",",
}
local function protect_segment(s)
return string.gsub(s, "([^A-Za-z0-9_])", function (c)
if segment_set[c] then return c
else return string.format("%%%02x", string.byte(c)) end
end)
end
-----------------------------------------------------------------------------
-- Encodes a string into its escaped hexadecimal representation
-- Input
-- s: binary string to be encoded
-- Returns
-- escaped representation of string binary
-----------------------------------------------------------------------------
function unescape(s)
return string.gsub(s, "%%(%x%x)", function(hex)
return string.char(tonumber(hex, 16))
end)
end
-----------------------------------------------------------------------------
-- Builds a path from a base path and a relative path
-- Input
-- base_path
-- relative_path
-- Returns
-- corresponding absolute path
-----------------------------------------------------------------------------
local function absolute_path(base_path, relative_path)
if string.sub(relative_path, 1, 1) == "/" then return relative_path end
local path = string.gsub(base_path, "[^/]*$", "")
path = path .. relative_path
path = string.gsub(path, "([^/]*%./)", function (s)
if s ~= "./" then return s else return "" end
end)
path = string.gsub(path, "/%.$", "/")
local reduced
while reduced ~= path do
reduced = path
path = string.gsub(reduced, "([^/]*/%.%./)", function (s)
if s ~= "../../" then return "" else return s end
end)
end
path = string.gsub(reduced, "([^/]*/%.%.)$", function (s)
if s ~= "../.." then return "" else return s end
end)
return path
end
-----------------------------------------------------------------------------
-- Parses a url and returns a table with all its parts according to RFC 2396
-- The following grammar describes the names given to the URL parts
-- <url> ::= <scheme>://<authority>/<path>;<params>?<query>#<fragment>
-- <authority> ::= <userinfo>@<host>:<port>
-- <userinfo> ::= <user>[:<password>]
-- <path> :: = {<segment>/}<segment>
-- Input
-- url: uniform resource locator of request
-- default: table with default values for each field
-- Returns
-- table with the following fields, where RFC naming conventions have
-- been preserved:
-- scheme, authority, userinfo, user, password, host, port,
-- path, params, query, fragment
-- Obs:
-- the leading '/' in {/<path>} is considered part of <path>
-----------------------------------------------------------------------------
function parse(url, default)
-- initialize default parameters
local parsed = {}
for i,v in pairs(default or parsed) do parsed[i] = v end
-- empty url is parsed to nil
if not url or url == "" then return nil, "invalid url" end
-- remove whitespace
-- url = string.gsub(url, "%s", "")
-- get fragment
url = string.gsub(url, "#(.*)$", function(f)
parsed.fragment = f
return ""
end)
-- get scheme
url = string.gsub(url, "^([%w][%w%+%-%.]*)%://",
function(s) parsed.scheme = s; return "" end)
-- get authority
url = string.gsub(url, "^([^/%?]*)", function(n)
parsed.authority = n
return ""
end)
-- get query stringing
url = string.gsub(url, "%?(.*)", function(q)
parsed.query = q
return ""
end)
-- get params
url = string.gsub(url, "%;(.*)", function(p)
parsed.params = p
return ""
end)
-- path is whatever was left
if url ~= "" then parsed.path = url else parsed.path = "/" end
local authority = parsed.authority
if not authority then return parsed end
authority = string.gsub(authority,"^([^@]*)@",
function(u) parsed.userinfo = u; return "" end)
authority = string.gsub(authority, ":([^:]*)$",
function(p) parsed.port = p; return "" end)
if authority ~= "" then parsed.host = authority end
local userinfo = parsed.userinfo
if not userinfo then return parsed end
userinfo = string.gsub(userinfo, ":([^:]*)$",
function(p) parsed.password = p; return "" end)
parsed.user = userinfo
return parsed
end
-----------------------------------------------------------------------------
-- Parses the url and also seperates the query terms into a table
-----------------------------------------------------------------------------
function parse2(url, default)
local parsed = parse(url, default)
if parsed.query then
local prmstr = parsed.query
local prmarr = string.Explode("&", prmstr)
local params = {}
for i = 1, #prmarr do
local tmparr = string.Explode("=", prmarr[i])
params[tmparr[1]] = tmparr[2]
end
parsed.query = params
end
if parsed.fragment then
local prmstr = parsed.fragment
local prmarr = string.Explode("&", prmstr)
local params = {}
for i = 1, #prmarr do
local tmparr = string.Explode("=", prmarr[i])
params[tmparr[1]] = tmparr[2]
end
parsed.fragment = params
end
return parsed
end
-----------------------------------------------------------------------------
-- Rebuilds a parsed URL from its components.
-- Components are protected if any reserved or unallowed characters are found
-- Input
-- parsed: parsed URL, as returned by parse
-- Returns
-- a stringing with the corresponding URL
-----------------------------------------------------------------------------
function build(parsed)
local ppath = parse_path(parsed.path or "")
local url = build_path(ppath)
local url = (parsed.path or ""):gsub("[^/]+", unescape)
local url = url:gsub("[^/]*", protect_segment)
if parsed.params then url = url .. ";" .. parsed.params end
if parsed.query then url = url .. "?" .. parsed.query end
local authority = parsed.authority
if parsed.host then
authority = parsed.host
if parsed.port then authority = authority .. ":" .. parsed.port end
local userinfo = parsed.userinfo
if parsed.user then
userinfo = parsed.user
if parsed.password then
userinfo = userinfo .. ":" .. parsed.password
end
end
if userinfo then authority = userinfo .. "@" .. authority end
end
if authority then url = "//" .. authority .. url end
if parsed.scheme then url = parsed.scheme .. ":" .. url end
if parsed.fragment then url = url .. "#" .. parsed.fragment end
-- url = string.gsub(url, "%s", "")
return url
end
-----------------------------------------------------------------------------
-- Builds a absolute URL from a base and a relative URL according to RFC 2396
-- Input
-- base_url
-- relative_url
-- Returns
-- corresponding absolute url
-----------------------------------------------------------------------------
function absolute(base_url, relative_url)
if type(base_url) == "table" then
base_parsed = base_url
base_url = build(base_parsed)
else
base_parsed = parse(base_url)
end
local relative_parsed = parse(relative_url)
if not base_parsed then return relative_url
elseif not relative_parsed then return base_url
elseif relative_parsed.scheme then return relative_url
else
relative_parsed.scheme = base_parsed.scheme
if not relative_parsed.authority or relative_parsed.authority == "" then
relative_parsed.authority = base_parsed.authority
if not relative_parsed.path then
relative_parsed.path = base_parsed.path
if not relative_parsed.params then
relative_parsed.params = base_parsed.params
if not relative_parsed.query then
relative_parsed.query = base_parsed.query
end
end
else
relative_parsed.path = absolute_path(base_parsed.path or "",
relative_parsed.path)
end
end
return build(relative_parsed)
end
end
-----------------------------------------------------------------------------
-- Breaks a path into its segments, unescaping the segments
-- Input
-- path
-- Returns
-- segment: a table with one entry per segment
-----------------------------------------------------------------------------
function parse_path(path)
local parsed = {}
path = path or ""
--path = string.gsub(path, "%s", "")
string.gsub(path, "([^/]*)", function (s) table.insert(parsed, s) end)
for i = 1, table.getn(parsed) do
parsed[i] = unescape(parsed[i])
end
if string.sub(path, 1, 1) == "/" then parsed.is_absolute = 1 end
if string.sub(path, -1, -1) == "/" then parsed.is_directory = 1 end
return parsed
end
-----------------------------------------------------------------------------
-- Builds a path component from its segments, escaping protected characters.
-- Input
-- parsed: path segments
-- unsafe: if true, segments are not protected before path is built
-- Returns
-- path: corresponding path stringing
-----------------------------------------------------------------------------
function build_path(parsed, unsafe)
local path = ""
local escape = unsafe and function(x) return x end or protect_segment
local n = table.getn(parsed)
for i = 1, n-1 do
if parsed[i]~= "" or parsed[i+1] == "" then
path = path .. escape(parsed[i])
if i < n - 1 or parsed[i+1] ~= "" then path = path .. "/" end
end
end
if n > 0 then
path = path .. escape(parsed[n])
if parsed.is_directory then path = path .. "/" end
end
if parsed.is_absolute then path = "/" .. path end
return path
end

View File

@ -0,0 +1,179 @@
---
-- EventEmitter
--
-- Based off of Wolfy87's JavaScript EventEmitter
--
local EventEmitter = {}
local function indexOfListener(listeners, listener)
local i = #listeners
while i > 0 do
if listeners[i].listener == listener then
return i
end
i = i - 1
end
return -1
end
function EventEmitter:new(obj)
if obj then
table.Inherit(obj, self)
else
return setmetatable({}, self)
end
end
function EventEmitter:getListeners(evt)
local events = self:_getEvents()
local response, key
-- TODO: accept pattern matching
if not events[evt] then
events[evt] = {}
end
response = events[evt]
return response
end
--[[function EventEmitter:flattenListeners(listeners)
end]]
function EventEmitter:getListenersAsObject(evt)
local listeners = self:getListeners(evt)
local response
if type(listeners) == 'table' then
response = {}
response[evt] = listeners
end
return response or listeners
end
function EventEmitter:addListener(evt, listener)
local listeners = self:getListenersAsObject(evt)
local listenerIsWrapped = type(listener) == 'table'
local key
for key, _ in pairs(listeners) do
if rawget(listeners, key) and indexOfListener(listeners[key], listener) == -1 then
table.insert(listeners[key], listenerIsWrapped and listener or {
listener = listener,
once = false
})
end
end
return self
end
EventEmitter.on = EventEmitter.addListener
function EventEmitter:addOnceListener(evt, listener)
return self:addListener(evt, {
listener = listener,
once = true
})
end
EventEmitter.once = EventEmitter.addOnceListener
function EventEmitter:removeListener(evt, listener)
local listeners = self:getListenersAsObject(evt)
local index, key
for key, _ in pairs(listeners) do
if rawget(listeners, key) then
index = indexOfListener(listeners[key], listener)
if index ~= -1 then
table.remove(listeners[key], index)
end
end
end
return self
end
EventEmitter.off = EventEmitter.removeListener
--[[function EventEmitter:addListeners(evt, listeners)
end]]
function EventEmitter:removeEvent(evt)
local typeStr = type(evt)
local events = self:_getEvents()
local key
if typeStr == 'string' then
events[evt] = nil
else
self._events = nil
end
return self
end
EventEmitter.removeAllListeners = EventEmitter.removeEvent
function EventEmitter:emitEvent(evt, ...)
local listeners = self:getListenersAsObject(evt)
local listener, i, key, response
for key, _ in pairs(listeners) do
if rawget(listeners, key) then
i = #listeners[key]
while i > 0 do
listener = listeners[key][i]
if listener.once == true then
self:removeListener(evt, listener.listener)
end
response = listener.listener(...)
if response == self:_getOnceReturnValue() then
self:removeListener(evt, listener.listener)
end
i = i - 1
end
end
end
return self
end
EventEmitter.trigger = EventEmitter.emitEvent
EventEmitter.emit = EventEmitter.emitEvent
function EventEmitter:setOnceReturnValue(value)
self._onceReturnValue = value
return self
end
function EventEmitter:_getOnceReturnValue()
if rawget(self, '_onceReturnValue') then
return self._onceReturnValue
else
return true
end
end
function EventEmitter:_getEvents()
if not rawget(self, '_events') then
self._events = {}
end
return self._events
end
_G.EventEmitter = EventEmitter

View File

@ -0,0 +1,299 @@
if browserpool then return end -- ignore Lua refresh
local table = table
local vgui = vgui
_G.browserpool = {}
---
-- Debug variable which will allow outputting messages if enabled.
-- @type boolean
--
local DEBUG = true
---
-- Array of available, pooled browsers
-- @type table
--
local available = {}
---
-- Array of active, pooled browsers.
-- @type table
--
local active = {}
---
-- Array of pending requests for a browser.
-- @type table
--
local pending = {}
---
-- Minimum number of active browsers to be pooled.
-- @type Number
--
local numMin = 2
---
-- Maximum number of active browsers to be pooled.
-- @type Number
--
local numMax = 5
---
-- Number of currently active browsers.
-- @type Number
--
local numActive = 0
---
-- Number of currently pending browser requests.
-- @type Number
--
local numPending = 0
---
-- Number of total browser requests.
-- @type Number
--
local numRequests = 0
---
-- Default URL to set browsers on setup/teardown.
-- @type String
--
local defaultUrl = "about:blank"
---
-- JavaScript code to remove an object's property.
-- @type String
--
local JS_RemoveProp = "delete %s.%s;"
---
-- Helper function to setup/teardown a browser panel.
--
-- @param panel? Browser panel to be cleaned up.
-- @return Panel DMediaPlayerHTML panel instance.
--
local function setupPanel( panel )
-- Create a new panel if it wasn't passed in
if panel then
panel:Stop()
else
panel = vgui.Create("DMediaPlayerHTML")
end
-- Hide panel
-- panel:SetSize(0, 0)
panel:SetPos(0, 0)
-- Disable input
panel:SetKeyBoardInputEnabled(false)
panel:SetMouseInputEnabled(false)
-- Browser panels are usually manually drawn, use a regular panel if not
panel:SetPaintedManually(true)
-- Set default URL
panel:OpenURL( defaultUrl )
-- Remove any added function callbacks
for obj, tbl in pairs(panel.Callbacks) do
if obj ~= "console" then
for funcname, _ in pairs(tbl) do
panel:QueueJavascript(JS_RemoveProp:format(obj, funcname))
end
end
end
return panel
end
---
-- Local function for removing cancelled browser promises via closures.
--
-- @param Promise Browser bromise.
-- @return Boolean Success status.
--
local function removePromise( promise )
local id = promise:GetId()
if not table.remove( pending, id ) then
ErrorNoHalt( "browserpool: Failed to remove promise.\n" )
print( promise, id )
debug.Trace()
return false
end
return true
end
---
-- Browser promise for resolving browser requests when there isn't an available
-- browser at the time of request.
--
local BrowserPromise = {}
local BrowserPromiseMeta = { __index = BrowserPromise }
function BrowserPromise:New( callback, id )
return setmetatable(
{ __cb = callback, __id = id or -1 },
BrowserPromiseMeta
)
end
function BrowserPromise:GetId()
return self.__id
end
function BrowserPromise:Resolve( value )
self.__cb(value)
end
function BrowserPromise:Cancel( reason )
self.__cb(false, reason)
removePromise(self)
end
---
-- Retrieves an available browser panel from the pool. Otherwise, a new panel
-- will be created.
--
-- @return Panel DMediaPlayerHTML panel instance.
--
function browserpool.get( callback )
numRequests = numRequests + 1
if DEBUG then
print( string.format("browserpool: get [Active: %s][Available: %s][Pending: %s]",
numActive, #available, numPending ) )
end
local panel
-- Check if there's an available panel
if #available > 0 then
panel = table.remove( available )
table.insert( active, panel )
callback( panel )
elseif numActive < numMax then -- create a new panel
panel = setupPanel()
numActive = numActive + 1
if DEBUG then
print( "browserpool: Spawned new browser [Active: "..numActive.."]" )
end
table.insert( active, panel )
callback( panel )
else -- wait for an available browser
local promise = BrowserPromise:New( callback, numRequests )
pending[numRequests] = promise
numPending = numPending + 1
return promise
end
end
---
-- Releases the given browser panel from the active pool.
--
-- Remember to unset references to the browser instance after releasing:
-- browserpool.release( self.Browser )
-- self.Browser = nil
--
-- @param panel Browser panel to be released.
-- @return boolean Whether the panel was successfully removed.
--
function browserpool.release( panel )
if not panel then return end
local key = table.KeyFromValue( active, panel )
-- Unable to find active browser panel
if not key then
ErrorNoHalt( "browserpool: Attempted to release unactive browser.\n" )
debug.Trace()
-- Remove browser even if the request was invalid
if ValidPanel(panel) then
panel:Remove()
end
return false
end
-- Resolve an open promise if one exists
if numPending > 0 then
-- Get the earliest request first
-- TODO: this seems to grab nil valued keys?
local id = table.GetFirstKey( pending )
local promise = pending[id]
pending[id] = nil
-- Cleanup panel
setupPanel( panel )
promise:Resolve( panel )
numPending = numPending - 1
else
if not table.remove( active, key ) then
ErrorNoHalt( "browserpool: Failed to remove panel from active browsers.\n" )
debug.Trace()
-- Remove browser even if the request was invalid
if ValidPanel(panel) then
panel:Remove()
end
return false
end
-- Remove panel if there are more active than the minimum pool size
if numActive > numMin then
panel:Remove()
numActive = numActive - 1
if DEBUG then
print( "browserpool: Destroyed browser [Active: "..numActive.."]" )
end
else
-- Cleanup panel
setupPanel( panel )
-- Add to the pool
table.insert( available, panel )
if DEBUG then
print( "browserpool: Pooled browser [Active: "..numActive.."]" )
end
end
end
return true
end

View File

@ -0,0 +1,96 @@
local IsValid = IsValid
local pairs = pairs
local RealTime = RealTime
local type = type
local IsKeyDown = input.IsKeyDown
local IsGameUIVisible = gui.IsGameUIVisible
local IsConsoleVisible = gui.IsConsoleVisible
_G.control = {}
local HoldTime = 0.3
local LastPress = nil
local LastKey = nil
local KeyControls = {}
local function dispatch(name, func, down, held)
-- Use same behavior as the hook system
if type(name) == 'table' then
if IsValid(name) then
func( name, down, held )
else
handles[ name ] = nil
end
else
func( down, held )
end
end
local function InputThink()
if IsGameUIVisible() or IsConsoleVisible() then return end
for key, handles in pairs( KeyControls ) do
for name, tbl in pairs( handles ) do
if tbl.Enabled then
-- Key hold (repeat press)
if tbl.LastPress and tbl.LastPress + HoldTime < RealTime() then
dispatch(name, tbl.Toggle, true, true)
tbl.LastPress = RealTime()
end
-- Key release
if not IsKeyDown( key ) then
dispatch(name, tbl.Toggle, false)
tbl.Enabled = false
end
else
-- Key press
if IsKeyDown( key ) then
dispatch(name, tbl.Toggle, true)
tbl.Enabled = true
tbl.LastPress = RealTime()
end
end
end
end
end
hook.Add( "Think", "InputManagerThink", InputThink )
function control.Add( key, name, onToggle )
if not (key and onToggle) then return end
if not KeyControls[ key ] then
KeyControls[ key ] = {}
end
KeyControls[ key ][ name ] = {
Enabled = false,
LastPress = 0,
Toggle = onToggle
--[[Toggle = function(...)
local msg, err = pcall( onToggle, ... )
if err then
print( "ERROR: " .. msg )
end
end]]
}
end
function control.Remove( key, name )
if not KeyControls[ key ] then return end
KeyControls[ key ][ name ] = nil
end

View File

@ -0,0 +1,140 @@
local cache = {}
local downloads = {}
local styles = {}
local embedHtml = [[
<!doctype html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<img id="mat" src="%s" width="100%%" height="100%%" />
<style type="text/css">
html, body {
width: 100%%;
height: 100%%;
margin: 0;
padding: 0;
overflow: hidden;
}
%s
</style>
<script type="text/javascript">
var mat = document.getElementById('mat');
mat.onload = function() {
setTimeout(function() { gmod.imageLoaded(); }, 100);
}
</script>
</body>
</html>]]
local UpdateTimerName = "HtmlMatUpdate"
local TimerRunning = false
local function updateCache(download)
download.browser:UpdateHTMLTexture()
cache[download.key] = download.browser:GetHTMLMaterial()
end
local function updateMaterials()
for _, download in ipairs(downloads) do
updateCache(download)
end
end
local function onImageLoaded(key, browser)
local idx
for k, v in pairs(downloads) do
if v.key == key then
idx = k
break
end
end
if idx > 0 then
local download = downloads[idx]
browserpool.release(browser)
table.remove(downloads, idx)
end
if #downloads == 0 and TimerRunning then
timer.Destroy(UpdateTimerName)
TimerRunning = false
end
end
local DefaultMat = Material("vgui/white")
local DefaultWidth = 128
local function enqueueUrl( url, styleName, key )
cache[key] = DefaultMat
browserpool.get(function(browser)
local style = styles[styleName] or {}
local w = style.width or DefaultWidth
local h = style.height or w
browser:SetSize( w, h )
local download = {
url = url,
key = key,
browser = browser
}
table.insert(downloads, download)
browser:AddFunction("gmod", "imageLoaded", function()
updateCache(download)
onImageLoaded(key, browser)
end)
if not TimerRunning then
timer.Create(UpdateTimerName, 0.05, 0, updateMaterials)
timer.Start(UpdateTimerName)
TimerRunning = true
end
local html = embedHtml:format(url, style.css or '')
browser:SetHTML( html )
end)
end
---
-- Renders a URL as a material.
--
-- @param url URL.
-- @param style HTMLMaterial style.
--
function HTMLMaterial( url, style )
local key
-- Build unique key for material
if style then
key = table.concat({url, '@', style})
else
key = url
end
-- Enqueue the URL to be downloaded if it hasn't been loaded yet.
if cache[key] == nil then
enqueueUrl( url, style, key )
end
-- Return cached URL
return cache[key]
end
---
-- Registers a style that can be used with `HTMLMaterial`
--
-- @param name Style name.
-- @param params Table of style parameters.
--
function AddHTMLMaterialStyle(name, params)
styles[name] = params
end

View File

@ -0,0 +1,81 @@
local function LoadMediaPlayer()
print( "Loading 'mediaplayer' addon..." )
-- Check if MediaPlayer has already been loaded
if MediaPlayer then
MediaPlayer.__refresh = true
-- HACK: Lua refresh fix; access local variable of baseclass lib
local _, BaseClassTable = debug.getupvalue(baseclass.Get, 1)
for classname, _ in pairs(BaseClassTable) do
if classname:find("mp_") then
BaseClassTable[classname] = nil
end
end
end
-- shared includes
IncludeCS "includes/extensions/sh_file.lua"
IncludeCS "includes/extensions/sh_math.lua"
IncludeCS "includes/extensions/sh_table.lua"
IncludeCS "includes/extensions/sh_url.lua"
IncludeCS "includes/modules/EventEmitter.lua"
if SERVER then
-- download clientside includes
AddCSLuaFile "includes/modules/browserpool.lua"
AddCSLuaFile "includes/modules/control.lua"
AddCSLuaFile "includes/modules/htmlmaterial.lua"
AddCSLuaFile "includes/extensions/cl_draw.lua"
-- initialize serverside mediaplayer
include "mediaplayer/init.lua"
else
-- clientside includes
include "includes/modules/browserpool.lua"
include "includes/modules/control.lua"
include "includes/modules/htmlmaterial.lua"
include "includes/extensions/cl_draw.lua"
-- initialize clientside mediaplayer
include "mediaplayer/cl_init.lua"
end
-- Sandbox includes; these must always be included as the gamemode is still
-- set as 'base' when the addon is loading. Can't check if gamemode derives
-- Sandbox.
IncludeCS "menubar/mp_options.lua"
include "properties/mediaplayer.lua"
if SERVER then
-- Reinstall media players on Lua refresh
for _, mp in pairs(MediaPlayer.GetAll()) do
if IsValid(mp.Entity) then
-- cache entity
local ent = mp.Entity
local queue = mp._Queue
local listeners = mp._Listeners
-- remove media player
mp:Remove()
-- install new media player
ent:InstallMediaPlayer()
-- reinitialize settings
mp = ent._mp
-- mp._Queue = queue
-- TODO: reapply listeners, for some reason the table is empty
-- after Lua refresh
mp:SetListeners( listeners )
end
end
end
end
-- First time load
LoadMediaPlayer()
-- Fix for Lua refresh not always working...
hook.Add( "OnReloaded", "MediaPlayerRefresh", LoadMediaPlayer )

View File

@ -0,0 +1,3 @@
MediaPlayer.Cvars.Resolution = CreateClientConVar( "mediaplayer_resolution", 480, true, false )
MediaPlayer.Cvars.Volume = CreateClientConVar( "mediaplayer_volume", 0.15, true, false )
MediaPlayer.Cvars.Fullscreen = CreateClientConVar( "mediaplayer_fullscreen", 0, true, false )

View File

@ -0,0 +1,55 @@
local DefaultIdlescreen = [[
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>MediaPlayer Idlescreen</title>
<style type="text/css">
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
}
* { box-sizing: border-box }
body {
background-color: #ececec;
color: #313131;
padding: 15px;
}
</style>
</head>
<body>
<h1>MediaPlayer Idlescreen</h1>
<p>Right-click on the media player to see a list of available actions.</p>
</body>
</html>
]]
function MediaPlayer.GetIdlescreen()
if not MediaPlayer._idlescreen then
local browser = vgui.Create( "DMediaPlayerHTML" )
browser:SetPaintedManually(true)
browser:SetKeyBoardInputEnabled(false)
browser:SetMouseInputEnabled(false)
browser:SetPos(0,0)
local resolution = MediaPlayer.Resolution()
browser:SetSize( resolution * 16/9, resolution )
-- TODO: set proper browser size
MediaPlayer._idlescreen = browser
local setup = hook.Run( "MediaPlayerSetupIdlescreen", browser )
if not setup then
MediaPlayer._idlescreen:SetHTML( DefaultIdlescreen )
end
end
return MediaPlayer._idlescreen
end

View File

@ -0,0 +1,254 @@
if MediaPlayer then
-- TODO: compare versions?
if MediaPlayer.__refresh then
MediaPlayer.__refresh = nil
else
return -- MediaPlayer has already been registered
end
end
include "controls/dmediaplayerhtml.lua"
include "controls/dhtmlcontrols.lua"
include "controls/dmediaplayerrequest.lua"
include "shared.lua"
include "cl_idlescreen.lua"
function MediaPlayer.Volume( volume )
if volume then
-- Normalize volume
volume = volume > 1 and volume/100 or volume
-- Set volume convar
RunConsoleCommand( "mediaplayer_volume", volume )
-- Apply volume to all media players
for _, mp in pairs(MediaPlayer.GetAll()) do
if mp:IsPlaying() then
local media = mp:CurrentMedia()
media:Volume( volume )
end
end
end
return MediaPlayer.Cvars.Volume:GetFloat()
end
function MediaPlayer.Resolution( resolution )
if resolution then
resolution = math.Clamp( resolution, 16, 4096 )
RunConsoleCommand( "mediaplayer_resolution", resolution )
end
return MediaPlayer.Cvars.Resolution:GetFloat()
end
-- TODO: Change to using a subscribe model rather than polling
function MediaPlayer.Poll( id )
net.Start( "MEDIAPLAYER.Update" )
net.WriteString( id )
net.SendToServer()
end
local function GetMediaPlayerId( obj )
local mpId
-- Determine mp parameter type and get the associated ID.
if isentity(obj) and obj.IsMediaPlayerEntity then
mpId = obj:GetMediaPlayerID()
-- elseif isentity(obj) and IsValid( obj:GetMediaPlayer() ) then
-- local mp = mp:GetMediaPlayer()
-- mpId = mp:GetId()
elseif istable(obj) and obj.IsMediaPlayer then
mpId = obj:GetId()
elseif isstring(obj) then
mpId = obj
else
return false -- Invalid parameters
end
return mpId
end
---
-- Request to begin listening to a media player.
--
-- @param Entity|Table|String Media player reference.
--
function MediaPlayer.RequestListen( obj )
local mpId = GetMediaPlayerId(obj)
if not mpId then return end
net.Start( "MEDIAPLAYER.RequestListen" )
net.WriteString( mpId )
net.SendToServer()
end
---
-- Request a URL to be played on the given media player.
--
-- @param Entity|Table|String Media player reference.
-- @param String Requested media URL.
--
function MediaPlayer.Request( obj, url )
local mpId = GetMediaPlayerId( obj )
if not mpId then return end
if MediaPlayer.DEBUG then
print("MEDIAPLAYER.Request:", url, mpId)
end
-- Verify valid URL as to not waste time networking
if not MediaPlayer.ValidUrl( url ) then
LocalPlayer():ChatPrint("The requested URL was invalid.")
return false
end
local media = MediaPlayer.GetMediaForUrl( url )
local function request( err )
if err then
-- TODO: don't use chatprint to notify the user
LocalPlayer():ChatPrint( "Request failed: " .. err )
return
end
net.Start( "MEDIAPLAYER.RequestMedia" )
net.WriteString( mpId )
net.WriteString( url )
media:NetWriteRequest() -- send any additional data
net.SendToServer()
if MediaPlayer.DEBUG then
print("MEDIAPLAYER.Request sent to server")
end
end
-- Prepare any data prior to requesting if necessary
if media.PrefetchMetadata then
media:PreRequest(request) -- async
else
request() -- sync
end
end
function MediaPlayer.Pause( mp )
local mpId = GetMediaPlayerId( mp )
if not mpId then return end
net.Start( "MEDIAPLAYER.RequestPause" )
net.WriteString( mpId )
net.SendToServer()
end
function MediaPlayer.Skip( mp )
local mpId = GetMediaPlayerId( mp )
if not mpId then return end
net.Start( "MEDIAPLAYER.RequestSkip" )
net.WriteString( mpId )
net.SendToServer()
end
---
-- Seek to a specific time in the current media.
--
-- @param Entity|Table|String Media player reference.
-- @param String Seek time; HH:MM:SS
--
function MediaPlayer.Seek( mp, time )
local mpId = GetMediaPlayerId( mp )
if not mpId then return end
net.Start( "MEDIAPLAYER.RequestSeek" )
net.WriteString( mpId )
net.WriteString( time )
net.SendToServer()
end
--[[---------------------------------------------------------
Utility functions
-----------------------------------------------------------]]
local FullscreenCvar = MediaPlayer.Cvars.Fullscreen
function MediaPlayer.SetBrowserSize( browser, w, h )
local fullscreen = FullscreenCvar:GetBool()
if fullscreen then
w, h = ScrW(), ScrH()
end
browser:SetSize( w, h, fullscreen )
end
function MediaPlayer.OpenRequestMenu( mp )
if ValidPanel(MediaPlayer._RequestMenu) then
return
end
local req = vgui.Create( "MPRequestFrame" )
req:SetMediaPlayer( mp )
req:MakePopup()
req:Center()
req.OnClose = function()
MediaPlayer._RequestMenu = nil
end
MediaPlayer._RequestMenu = req
end
function MediaPlayer.MenuRequest( url )
local menu = MediaPlayer._RequestMenu
if not ValidPanel(menu) then
return
end
local mp = menu:GetMediaPlayer()
menu:Close()
MediaPlayer.Request( mp, url )
end
--[[---------------------------------------------------------
Fonts
-----------------------------------------------------------]]
local common = {
-- font = "Open Sans Condensed",
-- font = "Oswald",
font = "Clear Sans Medium",
antialias = true,
weight = 400
}
surface.CreateFont( "MediaTitle", table.Merge(common, { size = 72 }) )
surface.CreateFont( "MediaRequestButton", table.Merge(common, { size = 26 }) )

View File

@ -0,0 +1,28 @@
--[[----------------------------------------------------------------------------
The Media Player server configuration contains API keys used for requesting
metadata for various services. All keys provided with the addon should not
be used elsewhere as to respect data usage limits.
------------------------------------------------------------------------------]]
MediaPlayer.SetConfig({
--[[------------------------------------------------------------------------
Google's Data API is used for YouTube and GoogleDrive requests. To
get your own API key, read through the following guide:
https://developers.google.com/youtube/v3/getting-started#intro
--------------------------------------------------------------------------]]
google = {
["api_key"] = "AIzaSyAjSwUHzyoxhfQZmiSqoIBQpawm2ucF11E",
["referrer"] = "http://mediaplayer.pixeltailgames.com/"
},
--[[------------------------------------------------------------------------
SoundCloud API
To register your own application, use the following webpage:
http://soundcloud.com/you/apps/new
--------------------------------------------------------------------------]]
soundcloud = {
["client_id"] = "2e0e541854cbabd873d647c1d45f79e8"
}
})

View File

@ -0,0 +1,349 @@
--[[__ _
/ _| __ _ ___ ___ _ __ _ _ _ __ ___| |__
| |_ / _` |/ __/ _ \ '_ \| | | | '_ \ / __| '_ \
| _| (_| | (_| __/ |_) | |_| | | | | (__| | | |
|_| \__,_|\___\___| .__/ \__,_|_| |_|\___|_| |_|
|_| 2010 --]]
--[[---------------------------------------------------------
Browser controls
-----------------------------------------------------------]]
local urllib = url
local PANEL = {}
AccessorFunc( PANEL, "HomeURL", "HomeURL", FORCE_STRING )
function PANEL:Init()
local ButtonSize = 32
local Margins = 2
local Spacing = 0
self.BackButton = vgui.Create( "DImageButton", self )
self.BackButton:SetSize( ButtonSize, ButtonSize )
self.BackButton:SetMaterial( "gui/HTML/back" )
self.BackButton:Dock( LEFT )
self.BackButton:DockMargin( Spacing*3, Margins, Spacing, Margins )
self.BackButton.DoClick = function()
self.BackButton:SetDisabled( true )
self:HTMLBack()
self.Cur = self.Cur - 1
self.Navigating = true
end
self.ForwardButton = vgui.Create( "DImageButton", self )
self.ForwardButton:SetSize( ButtonSize, ButtonSize )
self.ForwardButton:SetMaterial( "gui/HTML/forward" )
self.ForwardButton:Dock( LEFT )
self.ForwardButton:DockMargin( Spacing, Margins, Spacing, Margins )
self.ForwardButton.DoClick = function()
self.ForwardButton:SetDisabled( true )
self:HTMLForward()
self.Cur = self.Cur + 1
self.Navigating = true
end
self.RefreshButton = vgui.Create( "MPRefreshButton", self )
self.RefreshButton:SetSize( ButtonSize, ButtonSize )
self.RefreshButton:Dock( LEFT )
self.RefreshButton:DockMargin( Spacing, Margins, Spacing, Margins )
self.RefreshButton.DoClick = function()
self.RefreshButton:SetDisabled( true )
self.Refreshing = true
self.HTML:Refresh()
end
self.HomeButton = vgui.Create( "DImageButton", self )
self.HomeButton:SetSize( ButtonSize, ButtonSize )
self.HomeButton:SetMaterial( "gui/HTML/home" )
self.HomeButton:Dock( LEFT )
self.HomeButton:DockMargin( Spacing, Margins, Spacing*3, Margins )
self.HomeButton.DoClick = function()
self.HTML:Stop()
self.HTML:OpenURL( self:GetHomeURL() )
end
self.AddressBar = vgui.Create( "DTextEntry", self )
self.AddressBar:Dock( FILL )
self.AddressBar:DockMargin( Spacing, Margins * 3, Spacing, Margins * 3 )
self.AddressBar.OnEnter = function()
self.HTML:Stop()
self.HTML:OpenURL( self.AddressBar:GetValue() )
end
local AddrSetText = self.AddressBar.SetText
self.AddressBar.SetText = function (panel, text)
AddrSetText( panel, urllib.unescape(text) )
end
self.RequestButton = vgui.Create( "MPRequestButton", self )
self.RequestButton:SetDisabled( true )
self.RequestButton:Dock( RIGHT )
self.RequestButton:DockMargin( 8, 4, 8, 4 )
self.RequestButton.DoClick = function()
MediaPlayer.MenuRequest( self.HTML:GetURL() )
end
self:SetHeight( ButtonSize + Margins * 2 )
self.NavStack = 0
self.History = {}
self.Cur = 1
-- This is the default look, feel free to change it on your created control :)
self:SetButtonColor( Color( 250, 250, 250, 200 ) )
self.BorderSize = 4
self.BackgroundColor = Color( 33, 33, 33, 255 )
self:SetHomeURL( "http://www.google.com" )
end
function PANEL:SetHTML( html )
self.HTML = html
if ( html.URL ) then
self:SetHomeURL( self.HTML.URL )
end
self.RefreshButton:SetHTML(html)
self.AddressBar:SetText( self:GetHomeURL() )
self:UpdateHistory( self:GetHomeURL() )
local OldFunc = self.HTML.OpeningURL
self.HTML.OpeningURL = function( panel, url, target, postdata, bredirect )
self.NavStack = self.NavStack + 1
self.AddressBar:SetText( url )
self:StartedLoading()
if ( OldFunc ) then
OldFunc( panel, url, target, postdata, bredirect )
end
self:UpdateHistory( url )
end
local OldFunc = self.HTML.FinishedURL
self.HTML.FinishedURL = function( panel, url )
self.AddressBar:SetText( url )
self:FinishedLoading()
-- Check for valid URL
if MediaPlayer.ValidUrl( url ) then
self.RequestButton:SetDisabled( false )
else
self.RequestButton:SetDisabled( true )
end
if ( OldFunc ) then
OldFunc( panel, url )
end
end
end
function PANEL:UpdateHistory( url )
--print( "PANEL:UpdateHistory", url )
self.Cur = math.Clamp( self.Cur, 1, table.Count( self.History ) )
local top = self.History[self.Cur]
-- Ignore page refresh
if top == url then
return
end
if ( self.Refreshing ) then
self.Refreshing = false
self.RefreshButton:SetDisabled( false )
return
end
if ( self.Navigating ) then
self.Navigating = false
self:UpdateNavButtonStatus()
return
end
-- We were back in the history queue, but now we're navigating
-- So clear the front out so we can re-write history!!
if ( self.Cur < table.Count( self.History ) ) then
for i = self.Cur+1, table.Count( self.History ) do
self.History[i] = nil
end
end
self.Cur = table.insert( self.History, url )
self:UpdateNavButtonStatus()
end
function PANEL:HTMLBack()
if self.Cur <= 1 then return end
self.Cur = self.Cur - 1
self.HTML:OpenURL( self.History[ self.Cur ], true )
end
function PANEL:HTMLForward()
if self.Cur == #self.History then return end
self.Cur = self.Cur + 1
self.HTML:OpenURL( self.History[ self.Cur ], true )
end
function PANEL:FinishedLoading()
self.RefreshButton:SetDisabled( false )
end
function PANEL:StartedLoading()
self.RefreshButton:SetDisabled( true )
end
function PANEL:UpdateNavButtonStatus()
--print( self.Cur, table.Count( self.History ) )
self.ForwardButton:SetDisabled( self.Cur >= table.Count( self.History ) )
self.BackButton:SetDisabled( self.Cur == 1 )
end
function PANEL:SetButtonColor( col )
self.BackButton:SetColor( col )
self.ForwardButton:SetColor( col )
self.RefreshButton:SetColor( col )
self.HomeButton:SetColor( col )
end
function PANEL:Paint()
draw.RoundedBoxEx( self.BorderSize, 0, 0, self:GetWide(), self:GetTall(), self.BackgroundColor, true, true, false, false )
end
derma.DefineControl( "MPHTMLControls", "", PANEL, "Panel" )
--[[---------------------------------------------------------
Media request button
Embedded inside of the browser controls.
-----------------------------------------------------------]]
local RequestButton = {}
-- RequestButton.DisabledColor = Color(189, 195, 199)
-- RequestButton.DepressedColor = Color(192, 57, 43)
RequestButton.HoverColor = Color(192, 57, 43)
RequestButton.DefaultColor = Color(231, 76, 60)
RequestButton.DisabledColor = RequestButton.DefaultColor
RequestButton.DepressedColor = RequestButton.DefaultColor
RequestButton.DefaultTextColor = Color(236, 236, 236)
RequestButton.DisabledTextColor = Color(158, 48, 36)
function RequestButton:Init()
DButton.Init(self)
local ButtonSize = 32
self:SetSize( ButtonSize*8, ButtonSize )
self:SetFont( "MediaRequestButton" )
self:SetDisabled( true )
end
function RequestButton:SetDisabled( disabled )
if disabled then
self:SetText( "FIND A VIDEO" )
else
self:SetText( "REQUEST URL" )
end
DButton.SetDisabled( self, disabled )
end
function RequestButton:UpdateColours()
if self:GetDisabled() then
return self:SetTextStyleColor( self.DisabledTextColor )
else
return self:SetTextStyleColor( self.DefaultTextColor )
end
end
function RequestButton:Paint( w, h )
local col
if self:GetDisabled() then
col = self.DisabledColor
elseif self.Depressed or self.m_bSelected then
col = self.DepressedColor
elseif self:IsHovered() then
col = self.HoverColor
else
-- Pulse effect
local h, s, v = ColorToHSV( self.DefaultColor )
v = 0.7 + math.sin(RealTime() * 10) * 0.3
col = HSVToColor(h,s,v)
end
draw.RoundedBox( 2, 0, 0, w, h, col )
end
derma.DefineControl( "MPRequestButton", "", RequestButton, "DButton" )
--[[---------------------------------------------------------
Media refresh button
Embedded inside of the browser controls.
-----------------------------------------------------------]]
local RefreshButton = {}
AccessorFunc( RefreshButton, "HTML", "HTML" )
local LoadingTexture = surface.GetTextureID("gui/html/refresh")
function RefreshButton:Init()
DButton.Init(self)
self:SetText( "" )
self.TextureColor = Color(255,255,255,255)
end
function RefreshButton:SetColor( color )
self.TextureColor = color
end
function RefreshButton:Paint( w, h )
local ang = 0
if ValidPanel(self.HTML) and self.HTML:IsLoading() then
ang = RealTime() * -512
end
surface.SetDrawColor(self.TextureColor)
surface.SetTexture(LoadingTexture)
surface.DrawTexturedRectRotated( w/2, h/2, w, h, ang )
end
derma.DefineControl( "MPRefreshButton", "", RefreshButton, "DButton" )

View File

@ -0,0 +1,390 @@
local PANEL = {}
DEFINE_BASECLASS( "Panel" )
local JS_CallbackHack = [[(function(){
var funcname = '%s';
window[funcname] = function(){
_gm[funcname].apply(_gm,arguments);
}
})();]]
--[[---------------------------------------------------------
-----------------------------------------------------------]]
function PANEL:Init()
self.JS = {}
self.Callbacks = {}
self.URL = "about:blank"
--
-- Implement a console - because awesomium doesn't provide it for us anymore
--
local console_funcs = {'log','error','debug','warn','info'}
for _, func in pairs(console_funcs) do
self:AddFunction( "console", func, function(param)
self:ConsoleMessage( param, func )
end )
end
self:AddFunction( "gmod", "getUrl", function( url, finished )
self.URL = url
if finished then
self:FinishedURL( url )
else
self:OpeningURL( url )
end
end )
end
function PANEL:Think()
if self:IsLoading() then
-- Call started loading
if not self._loading then
-- Get the page URL
self:RunJavascript("gmod.getUrl(window.location.href, false);")
self._loading = true
self:OnStartLoading()
end
else
-- Call finished loading
if self._loading then
-- Get the page URL
self:RunJavascript("gmod.getUrl(window.location.href, true);")
-- Hack to add window object callbacks
if self.Callbacks.window then
for funcname, callback in pairs(self.Callbacks.window) do
self:RunJavascript( JS_CallbackHack:format(funcname) )
end
end
self._loading = nil
self:OnFinishLoading()
end
-- Run queued javascript
if self.JS then
for k, v in pairs( self.JS ) do
self:RunJavascript( v )
end
self.JS = nil
end
end
end
function PANEL:GetURL()
return self.URL
end
function PANEL:SetURL( url )
local current = self.URL
if current ~= url then
self:OnURLChanged( url, current )
end
self.URL = url
end
function PANEL:OnURLChanged( new, old )
end
--[[---------------------------------------------------------
Awesomium Override Functions
-----------------------------------------------------------]]
function PANEL:SetSize( w, h, fullscreen )
if fullscreen then
-- Cache fullscreen size
local cw, ch = self:GetSize()
self._OrigSize = { w = cw, h = ch }
-- Render before the HUD
self:ParentToHUD()
elseif self._OrigSize then
-- Restore cached size
w = self._OrigSize.w
h = self._OrigSize.h
self._OrigSize = nil
-- Reparent due to hud parented panels sometimes being inaccessible
-- from Lua.
self:SetParent( vgui.GetWorldPanel() )
else
self._OrigSize = nil
end
if not (w and h) then return end
BaseClass.SetSize( self, w, h )
end
function PANEL:OpenURL( url )
if MediaPlayer.DEBUG then
print("DMediaPlayerHTML.OpenURL", url)
end
self:SetURL( url )
BaseClass.OpenURL( self, url )
end
function PANEL:SetHTML( html )
if MediaPlayer.DEBUG then
print("DMediaPlayerHTML.SetHTML")
print(html)
end
BaseClass.SetHTML( self, html )
end
--[[function PANEL:RunJavascript( js )
if MediaPlayer.DEBUG then
print("DMediaPlayerHTML.RunJavascript", js)
end
BaseClass.RunJavascript( self, js )
end]]
--[[---------------------------------------------------------
Window loading events
-----------------------------------------------------------]]
--
-- Called when the page begins loading
--
function PANEL:OnStartLoading()
end
--
-- Called when the page finishes loading all assets
--
function PANEL:OnFinishLoading()
end
--[[---------------------------------------------------------
Lua => JavaScript queue
This code only runs when the page is finished loading;
this means all assets (images, CSS, etc.) must load first!
-----------------------------------------------------------]]
function PANEL:QueueJavascript( js )
--
-- Can skip using the queue if there's nothing else in it
--
if not ( self.JS or self:IsLoading() ) then
return self:RunJavascript( js )
end
self.JS = self.JS or {}
table.insert( self.JS, js )
self:Think()
end
PANEL.QueueJavaScript = PANEL.QueueJavascript
PANEL.Call = PANEL.QueueJavascript
--[[---------------------------------------------------------
Handle console logging from JavaScript
-----------------------------------------------------------]]
PANEL.ConsoleColors = {
["default"] = Color(255,160,255),
["text"] = Color(255,255,255),
["error"] = Color(235,57,65),
["warn"] = Color(227,181,23),
["info"] = Color(100,173,229),
}
function PANEL:ConsoleMessage( ... )
local args = {...}
local msg = args[1]
-- Three arguments are passed in if an error occured
if #args == 3 then
local script = args[2]
local linenum = args[3]
local col = self.ConsoleColors.error
local out = {
"[JavaScript]",
msg,
",",
script,
":",
linenum,
"\n"
}
MsgC( col, table.concat(out, " ") )
else
local func = args[2]
if not isstring( msg ) then
msg = "*js variable* (" .. type(msg) .. ": " .. tostring(msg) .. ")"
end
-- Run Lua from JavaScript console logging (POTENTIALLY HARMFUL!)
--[[if msg:StartWith( "RUNLUA:" ) then
local strLua = msg:sub( 8 )
SELF = self
RunString( strLua )
SELF = nil
return
end]]
-- Play a sound from JavaScript console logging
if msg:StartWith( "PLAY:" ) then
local soundpath = msg:sub( 7 )
surface.PlaySound( soundpath )
return
end
-- Output console message with prefix
local prefixColor = self.ConsoleColors.default
local prefix = "[HTML"
if func and func:len() > 0 and func ~= "log" then
if self.ConsoleColors[func] then
prefixColor = self.ConsoleColors[func]
end
prefix = prefix .. ":" .. func:upper()
end
prefix = prefix .. "] "
MsgC( prefixColor, prefix )
MsgC( self.ConsoleColors.text, msg, "\n" )
end
end
--[[---------------------------------------------------------
JavaScript callbacks
-----------------------------------------------------------]]
local JSObjects = {
window = "_gm",
this = "_gm",
_gm = "window"
}
--
-- Called by the engine when a callback function is called
--
function PANEL:OnCallback( obj, func, args )
-- Hack for adding window callbacks
obj = JSObjects[obj] or obj
if not self.Callbacks[ obj ] then return end
--
-- Use AddFunction to add functions to this.
--
local f = self.Callbacks[ obj ][ func ]
if ( f ) then
return f( unpack( args ) )
end
end
--
-- Add a function to Javascript
--
function PANEL:AddFunction( obj, funcname, func )
-- Hack for adding window callbacks
-- obj = JSObjects[obj] or obj
if obj == "this" then
obj = "window"
end
-- Create the `object` if it doesn't exist
if not self.Callbacks[ obj ] then
self:NewObject( obj )
self.Callbacks[ obj ] = {}
end
-- This creates the function in javascript (which redirects to c++ which calls OnCallback here)
self:NewObjectCallback( JSObjects[obj] or obj, funcname )
-- Store the function so OnCallback can find it and call it
self.Callbacks[ obj ][ funcname ] = func
end
--[[---------------------------------------------------------
Remove Scrollbars
-----------------------------------------------------------]]
-- TODO: Change to appending CSS style?
-- ::-webkit-scrollbar { visibility: hidden; }
local JS_RemoveScrollbars = "document.body.style.overflow = 'hidden';"
function PANEL:RemoveScrollbars()
self:QueueJavascript(JS_RemoveScrollbars)
end
--[[---------------------------------------------------------
Compatibility functions
-----------------------------------------------------------]]
function PANEL:OpeningURL( url )
end
function PANEL:FinishedURL( url )
end
derma.DefineControl( "DMediaPlayerHTML", "", PANEL, "Awesomium" )

View File

@ -0,0 +1,128 @@
local PANEL = {}
PANEL.HistoryWidth = 300
PANEL.BackgroundColor = Color(22, 22, 22)
local CloseTexture = Material( "theater/close.png" )
AccessorFunc( PANEL, '_mp', 'MediaPlayer' )
function PANEL:Init()
self:SetPaintBackgroundEnabled( true )
self:SetFocusTopLevel( true )
local w = math.Clamp( ScrW() - 100, 800, 1152 + self.HistoryWidth )
local h = ScrH()
if h > 800 then
h = h * 3/4
elseif h > 600 then
h = h * 7/8
end
self:SetSize( w, h )
self.CloseButton = vgui.Create( "DButton", self )
self.CloseButton:SetZPos( 5 )
self.CloseButton:NoClipping( true )
self.CloseButton:SetText( "" )
self.CloseButton.DoClick = function ( button )
self:Close()
end
self.CloseButton.Paint = function( panel, w, h )
DisableClipping( true )
surface.SetDrawColor( 48, 55, 71 )
surface.DrawRect( 2, 2, w - 4, h - 4 )
surface.SetDrawColor( 26, 30, 38 )
surface.SetMaterial( CloseTexture )
surface.DrawTexturedRect( 0, 0, w, h )
DisableClipping( false )
end
self.BrowserContainer = vgui.Create( "DPanel", self )
self.Browser = vgui.Create( "DMediaPlayerHTML", self.BrowserContainer )
self.Browser:AddFunction( "gmod", "requestUrl", function (url)
MediaPlayer.MenuRequest( url )
self:Close()
end )
self.Browser:OpenURL( "http://gmtower.org/apps/mediaplayer/" )
self.Controls = vgui.Create( "MPHTMLControls", self.BrowserContainer )
self.Controls:SetHTML( self.Browser )
self.Controls.BorderSize = 0
-- Listen for all mouse press events
hook.Add( "GUIMousePressed", self, self.OnGUIMousePressed )
end
function PANEL:Paint( w, h )
-- Draw background for fully transparent webpages
surface.SetDrawColor( self.BackgroundColor )
surface.DrawRect( 0, 0, w, h )
return true
end
function PANEL:OnRemove()
hook.Remove( "GUIMousePressed", self )
end
function PANEL:Close()
if ValidPanel(self.Browser) then
self.Browser:Remove()
end
self:OnClose()
self:Remove()
end
function PANEL:OnClose()
end
function PANEL:CheckClose()
local x, y = self:CursorPos()
-- Remove panel if mouse is clicked outside of itself
if not (gui.IsGameUIVisible() or gui.IsConsoleVisible()) and
( x < 0 or x > self:GetWide() or y < 0 or y > self:GetTall() ) then
self:Close()
end
end
function PANEL:PerformLayout()
local w, h = self:GetSize()
self.CloseButton:SetSize( 32, 32 )
self.CloseButton:SetPos( w - 34, 2 )
self.BrowserContainer:Dock( FILL )
self.Browser:Dock( FILL )
self.Controls:Dock( TOP )
self.Controls:DockPadding( 0, 0, 32, 0 )
end
---
-- Close the panel when the mouse has been pressed outside of the panel.
--
function PANEL:OnGUIMousePressed( key )
if key == MOUSE_LEFT then
self:CheckClose()
end
end
vgui.Register( "MPRequestFrame", PANEL, "EditablePanel" )

View File

@ -0,0 +1,160 @@
if MediaPlayer then
-- TODO: compare versions?
if MediaPlayer.__refresh then
MediaPlayer.__refresh = nil
else
return -- MediaPlayer has already been registered
end
end
resource.AddFile "resource/fonts/ClearSans-Medium.ttf"
AddCSLuaFile "controls/dmediaplayerhtml.lua"
AddCSLuaFile "controls/dhtmlcontrols.lua"
AddCSLuaFile "controls/dmediaplayerrequest.lua"
AddCSLuaFile "cl_init.lua"
AddCSLuaFile "cl_idlescreen.lua"
AddCSLuaFile "shared.lua"
AddCSLuaFile "sh_mediaplayer.lua"
AddCSLuaFile "sh_services.lua"
AddCSLuaFile "sh_history.lua"
include "shared.lua"
include "sv_metadata.lua"
local function NetReadMediaPlayer()
local mpId = net.ReadString()
local mp = MediaPlayer.GetById(mpId)
if not IsValid(mp) then
if MediaPlayer.DEBUG then
print("MEDIAPLAYER.Request: Invalid media player ID", mpId, mp, ply)
end
return false
end
return mp
end
--[[---------------------------------------------------------
Request
-----------------------------------------------------------]]
util.AddNetworkString( "MEDIAPLAYER.RequestListen" )
util.AddNetworkString( "MEDIAPLAYER.RequestMedia" )
util.AddNetworkString( "MEDIAPLAYER.RequestPause" )
util.AddNetworkString( "MEDIAPLAYER.RequestSkip" )
util.AddNetworkString( "MEDIAPLAYER.RequestSeek" )
local ListenDelay = 0.5 -- seconds
local function OnListenRequest( len, ply )
if not IsValid(ply) then return end
if ply._NextListen and ply._NextListen > CurTime() then
return
end
local mpId = net.ReadString()
local mp = MediaPlayer.GetById(mpId)
if not IsValid(mp) then return end
if MediaPlayer.DEBUG then
print("MEDIAPLAYER.RequestListen:", mpId, ply)
end
-- TODO: check if listener can actually be a listener
if mp:HasListener(ply) then
mp:RemoveListener(ply)
else
mp:AddListener(ply)
end
ply._NextListen = CurTime() + ListenDelay
end
net.Receive( "MEDIAPLAYER.RequestListen", OnListenRequest )
local function OnMediaRequest( len, ply )
if not IsValid(ply) then return end
-- TODO: impose request delay for player
local mp = NetReadMediaPlayer()
if not mp then return end
local url = net.ReadString()
if MediaPlayer.DEBUG then
print("MEDIAPLAYER.RequestMedia:", url, mp:GetId(), ply)
end
-- Validate the URL
if not MediaPlayer.ValidUrl( url ) then
ply:ChatPrint( "The requested URL wasn't valid." )
return
end
-- Build the media object for the URL
local media = MediaPlayer.GetMediaForUrl( url )
media:NetReadRequest()
mp:RequestMedia( media, ply )
end
net.Receive( "MEDIAPLAYER.RequestMedia", OnMediaRequest )
local function OnPauseMedia( len, ply )
if not IsValid(ply) then return end
local mp = NetReadMediaPlayer()
if not mp then return end
if MediaPlayer.DEBUG then
print("MEDIAPLAYER.RequestPause:", mp:GetId(), ply)
end
mp:RequestPause( ply )
end
net.Receive( "MEDIAPLAYER.RequestPause", OnPauseMedia )
local function OnSkipMedia( len, ply )
if not IsValid(ply) then return end
local mp = NetReadMediaPlayer()
if not mp then return end
if MediaPlayer.DEBUG then
print("MEDIAPLAYER.RequestSkip:", mp:GetId(), ply)
end
mp:RequestSkip( ply )
end
net.Receive( "MEDIAPLAYER.RequestSkip", OnSkipMedia )
local function OnSeekMedia( len, ply )
if not IsValid(ply) then return end
local mp = NetReadMediaPlayer()
if not mp then return end
if MediaPlayer.DEBUG then
print("MEDIAPLAYER.RequestSeek:", mp:GetId(), ply)
end
local seekTime = net.ReadString()
mp:RequestSeek( ply, seekTime )
end
net.Receive( "MEDIAPLAYER.RequestSeek", OnSeekMedia )

View File

@ -0,0 +1,15 @@
-- TODO: mixins will be used for adding common functionality to mediaplayer
-- types. In this case, voting functionality for items in the media queue.
local VOTE = {}
function VOTE:New( ply, value )
local obj = setmetatable({}, self)
self.Player = ply
self.value = value
return obj
end
function MEDIAPLAYER:

View File

@ -0,0 +1,171 @@
local pcall = pcall
local Color = Color
local RealTime = RealTime
local ValidPanel = ValidPanel
local Vector = Vector
local cam = cam
local draw = draw
local math = math
local string = string
local surface = surface
local TEXT_ALIGN_CENTER = draw.TEXT_ALIGN_CENTER
local TEXT_ALIGN_TOP = draw.TEXT_ALIGN_TOP
local TEXT_ALIGN_BOTTOM = draw.TEXT_ALIGN_BOTTOM
local TEXT_ALIGN_LEFT = draw.TEXT_ALIGN_LEFT
local TEXT_ALIGN_RIGHT = draw.TEXT_ALIGN_RIGHT
local TextPaddingX = 12
local TextPaddingY = 12
local TextBoxPaddingX = 8
local TextBoxPaddingY = 2
local TextBgColor = Color(0, 0, 0, 200)
local BarBgColor = Color(0, 0, 0, 200)
local BarFgColor = Color(255, 255, 255, 255)
local function DrawText( text, font, x, y, xalign, yalign )
return draw.SimpleText( text, font, x, y, color_white, xalign, yalign )
end
local function DrawTextBox( text, font, x, y, xalign, yalign )
xalign = xalign or TEXT_ALIGN_LEFT
yalign = yalign or TEXT_ALIGN_TOP
surface.SetFont( font )
tw, th = surface.GetTextSize( text )
if xalign == TEXT_ALIGN_CENTER then
x = x - tw/2
elseif xalign == TEXT_ALIGN_RIGHT then
x = x - tw
end
if yalign == TEXT_ALIGN_CENTER then
y = y - th/2
elseif yalign == TEXT_ALIGN_BOTTOM then
y = y - th
end
surface.SetDrawColor( TextBgColor )
surface.DrawRect( x, y,
tw + TextBoxPaddingX * 2,
th + TextBoxPaddingY * 2 )
end
local UTF8SubLastCharPattern = "[^\128-\191][\128-\191]*$"
local OverflowString = "..." -- ellipsis
---
-- Limits a rendered string's width based on a maximum width.
--
-- @param text Text string.
-- @param font Font.
-- @param w Maximum width.
-- @return String String fitting the maximum required width.
--
local function RestrictStringWidth( text, font, w )
-- TODO: Cache this
surface.SetFont( font )
local curwidth = surface.GetTextSize( text )
local overflow = false
-- Reduce text by one character until it fits
while curwidth > w do
-- Text has overflowed, append overflow string on return
if not overflow then
overflow = true
end
-- Cut off last character
text = string.gsub(text, UTF8SubLastCharPattern, "")
-- Check size again
curwidth = surface.GetTextSize( text .. OverflowString )
end
return overflow and (text .. OverflowString) or text
end
function MEDIAPLAYER:DrawHTML( browser, w, h )
surface.SetDrawColor( 0, 0, 0, 255 )
surface.DrawRect( 0, 0, w, h )
draw.HTMLTexture( browser, w, h )
end
function MEDIAPLAYER:DrawMediaInfo( media, w, h )
-- TODO: Fadeout media info instead of just hiding
if not vgui.CursorVisible() and RealTime() - self._LastMediaUpdate > 3 then
return
end
-- Text dimensions
local tw, th
-- Title background
local titleStr = RestrictStringWidth( media:Title(), "MediaTitle",
w - (TextPaddingX * 2 + TextBoxPaddingX * 2) )
DrawTextBox( titleStr, "MediaTitle", TextPaddingX, TextPaddingY )
-- Title
DrawText( titleStr, "MediaTitle",
TextPaddingX + TextBoxPaddingX,
TextPaddingY + TextBoxPaddingY )
-- Track bar
if media:IsTimed() then
local duration = media:Duration()
local curTime = media:CurrentTime()
local percent = math.Clamp( curTime / duration, 0, 1 )
-- Bar height
local bh = math.Round(h * 1/32)
-- Bar background
draw.RoundedBox( 0, 0, h - bh, w, bh, BarBgColor )
-- Bar foreground (progress)
draw.RoundedBox( 0, 0, h - bh, w * percent, bh, BarFgColor )
local timeY = h - bh - TextPaddingY * 2
-- Current time
local curTimeStr = string.FormatSeconds(math.Clamp(math.Round(curTime), 0, duration))
DrawTextBox( curTimeStr, "MediaTitle", TextPaddingX, timeY,
TEXT_ALIGN_LEFT, TEXT_ALIGN_BOTTOM )
DrawText( curTimeStr, "MediaTitle", TextPaddingX * 2, timeY,
TEXT_ALIGN_LEFT, TEXT_ALIGN_BOTTOM )
-- Duration
local durationStr = string.FormatSeconds( duration )
DrawTextBox( durationStr, "MediaTitle", w - TextPaddingX * 2, timeY,
TEXT_ALIGN_RIGHT, TEXT_ALIGN_BOTTOM )
DrawText( durationStr, "MediaTitle", w - TextBoxPaddingX * 2, timeY,
TEXT_ALIGN_RIGHT, TEXT_ALIGN_BOTTOM )
end
-- Volume
local volume = MediaPlayer.Volume()
local volumeStr = tostring( math.Round( volume * 100 ) )
-- DrawText( volumeStr, "MediaTitle", w - TextPaddingX, h/2,
-- TEXT_ALIGN_CENTER )
-- Loading indicator
end

View File

@ -0,0 +1,78 @@
local pcall = pcall
local Color = Color
local RealTime = RealTime
local ScrW = ScrW
local ScrH = ScrH
local ValidPanel = ValidPanel
local Vector = Vector
local cam = cam
local draw = draw
local math = math
local string = string
local surface = surface
local FullscreenCvar = MediaPlayer.Cvars.Fullscreen
--[[---------------------------------------------------------
Convar callback
-----------------------------------------------------------]]
local function OnFullscreenConVarChanged( name, old, new )
local media
for _, mp in pairs(MediaPlayer.List) do
mp._LastMediaUpdate = RealTime()
media = mp:CurrentMedia()
if IsValid(media) and ValidPanel(media.Browser) then
MediaPlayer.SetBrowserSize( media.Browser )
end
end
MediaPlayer.SetBrowserSize( MediaPlayer.GetIdlescreen() )
end
cvars.AddChangeCallback( FullscreenCvar:GetName(), OnFullscreenConVarChanged )
--[[---------------------------------------------------------
Draw functions
-----------------------------------------------------------]]
function MEDIAPLAYER:DrawFullscreen()
-- Don't draw if we're not fullscreen
if not FullscreenCvar:GetBool() then return end
local w, h = ScrW(), ScrH()
local media = self:CurrentMedia()
if IsValid(media) then
-- Custom media draw function
if media.Draw then
media:Draw( w, h )
end
-- TODO: else draw 'not yet implemented' screen?
-- Draw media info
local succ, err = pcall( self.DrawMediaInfo, self, media, w, h )
if not succ then
print( err )
end
else
local browser = MediaPlayer.GetIdlescreen()
if ValidPanel(browser) then
self:DrawHTML( browser, w, h )
end
end
end

View File

@ -0,0 +1,140 @@
include "shared.lua"
include "cl_draw.lua"
include "cl_fullscreen.lua"
include "cl_net.lua"
function MEDIAPLAYER:NetReadUpdate()
-- Allows for another media player type to extend update net messages
end
function MEDIAPLAYER:OnQueueKeyPressed( down, held )
self._LastMediaUpdate = RealTime()
end
local function OnMediaUpdate( len )
local mpId = net.ReadString()
local mpType = net.ReadString()
if MediaPlayer.DEBUG then
print( "Received MEDIAPLAYER.Update", mpId, mpType )
end
local mp = MediaPlayer.GetById(mpId)
if not mp then
mp = MediaPlayer.Create( mpId, mpType )
end
local state = mp.net.ReadPlayerState()
-- Read extended update information
mp:NetReadUpdate()
-- Clear old queue
mp:ClearMediaQueue()
-- Read queue information
local count = net.ReadUInt( math.CeilPower2(mp.MaxMediaItems)/2 )
for i = 1, count do
local media = mp.net.ReadMedia()
mp:AddMedia(media)
end
mp:SetPlayerState( state )
hook.Call( "OnMediaPlayerUpdate", mp )
end
net.Receive( "MEDIAPLAYER.Update", OnMediaUpdate )
local function OnMediaSet( len )
if MediaPlayer.DEBUG then
print( "Received MEDIAPLAYER.Media" )
end
local mpId = net.ReadString()
local mp = MediaPlayer.GetById(mpId)
if not mp then
if MediaPlayer.DEBUG then
ErrorNoHalt("Received media for invalid mediaplayer\n")
print("ID: " .. tostring(mpId))
debug.Trace()
end
return
end
if mp:GetPlayerState() >= MP_STATE_PLAYING then
mp:OnMediaFinished()
end
local media = mp.net.ReadMedia()
if media then
local startTime = net.ReadInt(32)
media:StartTime( startTime )
local state = mp:GetPlayerState()
if state == MP_STATE_PLAYING then
media:Play()
else
media:Pause()
end
end
mp:SetMedia( media )
end
net.Receive( "MEDIAPLAYER.Media", OnMediaSet )
local function OnMediaRemoved( len )
if MediaPlayer.DEBUG then
print( "Received MEDIAPLAYER.Remove" )
end
local mpId = net.ReadString()
local mp = MediaPlayer.GetById(mpId)
if not mp then return end
mp:Remove()
end
net.Receive( "MEDIAPLAYER.Remove", OnMediaRemoved )
local function OnMediaSeek( len )
local mpId = net.ReadString()
local mp = MediaPlayer.GetById(mpId)
if not ( mp and (mp:GetPlayerState() >= MP_STATE_PLAYING) ) then return end
local startTime = net.ReadInt(32)
if MediaPlayer.DEBUG then
print( "Received MEDIAPLAYER.Seek", mpId, startTime )
end
local media = mp:CurrentMedia()
media:StartTime( startTime )
end
net.Receive( "MEDIAPLAYER.Seek", OnMediaSeek )
local function OnMediaPause( len )
local mpId = net.ReadString()
local mp = MediaPlayer.GetById(mpId)
if not mp then return end
local state = mp.net.ReadPlayerState()
if MediaPlayer.DEBUG then
print( "Received MEDIAPLAYER.Pause", mpId, state )
end
mp:SetPlayerState( state )
end
net.Receive( "MEDIAPLAYER.Pause", OnMediaPause )

View File

@ -0,0 +1,42 @@
local net = net
local EOT = "\4" -- End of transmission
MEDIAPLAYER.net = {}
local mpnet = MEDIAPLAYER.net
function mpnet.ReadDuration()
return net.ReadUInt(16)
end
function mpnet.ReadMedia()
local url = net.ReadString()
if url == EOT then
return nil
end
local title = net.ReadString()
local duration = mpnet.ReadDuration()
local ownerName = net.ReadString()
local ownerSteamId = net.ReadString()
-- Create media object
local media = MediaPlayer.GetMediaForUrl( url )
-- Set metadata
media._metadata = {
title = title,
duration = duration
}
media._OwnerName = ownerName
media._OwnerSteamID = ownerSteamId
return media
end
local StateBits = math.CeilPower2(NUM_MP_STATE) / 2
function mpnet.ReadPlayerState()
return net.ReadUInt(StateBits)
end

View File

@ -0,0 +1,490 @@
AddCSLuaFile "shared.lua"
AddCSLuaFile "cl_draw.lua"
AddCSLuaFile "cl_fullscreen.lua"
AddCSLuaFile "cl_net.lua"
include "shared.lua"
include "sv_net.lua"
-- Additional transmit states
TRANSMIT_LOCATION = 4
-- Media player network strings
util.AddNetworkString( "MEDIAPLAYER.Update" )
util.AddNetworkString( "MEDIAPLAYER.Media" )
util.AddNetworkString( "MEDIAPLAYER.Remove" )
util.AddNetworkString( "MEDIAPLAYER.Pause" )
util.AddNetworkString( "MEDIAPLAYER.Seek" )
--[[---------------------------------------------------------
Listeners
-----------------------------------------------------------]]
function MEDIAPLAYER:UpdateListeners()
local transmitState = self._TransmitState
local listeners = nil
if transmitState == TRANSMIT_NEVER then
return
elseif transmitState == TRANSMIT_ALWAYS then
listeners = player.GetAll()
elseif transmitState == TRANSMIT_PVS then
listeners = player.GetInPVS( self.Entity and self.Entity or self:GetPos() )
elseif transmitState == TRANSMIT_LOCATION then
local loc = self:GetLocation()
if not Location then
ErrorNoHalt("'Location' module not defined in mediaplayer!\n")
debug.Trace()
return
elseif loc == -1 then
ErrorNoHalt("Invalid location assigned to mediaplayer!\n")
debug.Trace()
return
end
listeners = Location.GetPlayersInLocation( loc )
else
ErrorNoHalt("Invalid transmit state for mediaplayer\n")
debug.Trace()
return
end
self:SetListeners(listeners)
end
function MEDIAPLAYER:SetListeners( listeners )
local ValidListeners = {}
-- Filter listeners
for _, ply in pairs(listeners) do
if IsValid(ply) and ply:IsConnected() and not ply:IsBot() then
table.insert( ValidListeners, ply )
end
end
-- Find players who should be removed.
--
-- A = self._Listeners
-- B = listeners
-- (A ∩ B)^c
for _, ply in pairs(self._Listeners) do
if not table.HasValue( ValidListeners, ply ) then
self:RemoveListener( ply )
end
end
-- Find players who should be added
for _, ply in pairs(ValidListeners) do
if not self:HasListener(ply) then
self:AddListener( ply )
end
end
end
function MEDIAPLAYER:AddListener( ply )
if MediaPlayer.DEBUG then
print( "MEDIAPLAYER.AddListener", self, ply )
end
table.insert( self._Listeners, ply )
-- Send player queue information
self:BroadcastUpdate(ply)
-- Send current media to new listener
if (self:GetPlayerState() >= MP_STATE_PLAYING) then
self:SendMedia( self:CurrentMedia(), ply )
end
hook.Call( "MediaPlayerAddListener", self, ply )
end
function MEDIAPLAYER:RemoveListener( ply )
if MediaPlayer.DEBUG then
print( "MEDIAPLAYER.RemoveListener", self, ply )
end
local key = table.RemoveByValue( self._Listeners, ply )
if not key then
ErrorNoHalt( "Tried to remove player from media player " ..
"who wasn't listening\n" )
debug.Trace()
return
end
-- Inform listener of removal
net.Start( "MEDIAPLAYER.Remove" )
net.WriteString( self:GetId() )
net.Send( ply )
hook.Call( "MediaPlayerRemoveListener", self, ply )
end
function MEDIAPLAYER:HasListener( ply )
return table.HasValue( self._Listeners, ply )
end
--[[---------------------------------------------------------
Queue Management
-----------------------------------------------------------]]
function MEDIAPLAYER:NextMedia()
if MediaPlayer.DEBUG then
print( "MEDIAPLAYER.NextMedia" )
end
local media = nil
-- Grab media from the queue if available
if not self:IsQueueEmpty() then
media = table.remove( self._Queue, 1 )
end
self:SetMedia( media )
self:SendMedia( media )
self:BroadcastUpdate()
end
function MEDIAPLAYER:SendMedia( media, ply )
if MediaPlayer.DEBUG then
print( "MEDIAPLAYER.SendMedia", media )
end
self:UpdateListeners()
local startTime = media and media:StartTime() or 0
net.Start( "MEDIAPLAYER.Media" )
net.WriteString( self:GetId() )
self.net.WriteMedia( media )
net.WriteInt( startTime, 32 )
net.Send( ply or self._Listeners )
end
--[[---------------------------------------------------------
Media requests
-----------------------------------------------------------]]
---
-- Determine whether the player is allowed to request media. Override this for
-- custom behavior.
--
-- @return boolean
--
function MEDIAPLAYER:CanPlayerRequestMedia( ply, media )
-- Check service whitelist if it exists on the mediaplayer
if self.ServiceWhitelist and not table.HasValue(self.ServiceWhitelist, media.Id) then
local names = MediaPlayer.GetValidServiceNames(self.ServiceWhitelist)
local msg = "The requested media isn't supported; accepted services are as followed:\n"
msg = msg .. table.concat( names, ", " )
self:NotifyPlayer( ply, msg )
return false
end
return true
end
---
-- Determines if the player has privileges to use media controls (skip, seek,
-- etc.). Override this for custom behavior.
--
function MEDIAPLAYER:IsPlayerPrivileged( ply )
return ply == self:GetOwner() or ply:IsAdmin()
end
---
-- Determine whether the media should be added to the queue.
-- This should be overwritten if only certain media should be allowed.
--
-- @return boolean
--
function MEDIAPLAYER:ShouldAddMedia( media )
return true
end
-- TODO: Remove this function in favor of RequestMedia
function MEDIAPLAYER:RequestUrl( url, ply )
if not IsValid(ply) then return end
if MediaPlayer.DEBUG then
print( "MEDIAPLAYER.RequestUrl", url, ply )
end
-- Queue must have space for the request
if #self._Queue == self.MaxMediaItems then
self:NotifyPlayer( ply, "The media player queue is full." )
return
end
-- Validate the URL
if not MediaPlayer.ValidUrl( url ) then
self:NotifyPlayer( ply, "The requested URL wasn't valid." )
return
end
-- Build the media object for the URL
local media = MediaPlayer.GetMediaForUrl( url )
self:RequestMedia( media, ply )
end
function MEDIAPLAYER:RequestMedia( media, ply )
-- Player must be valid and also a listener
if not ( IsValid(ply) and self:HasListener(ply) and
self:CanPlayerRequestMedia(ply, media) ) then
return
end
if MediaPlayer.DEBUG then
print( "MEDIAPLAYER.RequestMedia", media, ply )
end
-- Queue must have space for the request
if #self._Queue == self.MaxMediaItems then
self:NotifyPlayer( ply, "The media player queue is full." )
return
end
-- Make sure the media isn't already in the queue
for _, s in pairs(self._Queue) do
if s.Id == media.Id and s:UniqueID() == media:UniqueID() then
if MediaPlayer.DEBUG then
print("MediaPlayer.RequestMedia: Duplicate request", s.Id, media.Id)
print(media)
print(s)
end
self:NotifyPlayer( ply, "The requested media was already in the queue" )
return
end
end
-- TODO: prevent media from playing if this hook returns false(?)
hook.Run( "PreMediaPlayerMediaRequest", self, media, ply )
-- self:NotifyPlayer( ply, "Processing media request..." )
-- Fetch the media's metadata
media:GetMetadata(function(data, err)
if not data then
err = err and err or "There was a problem fetching the requested media's metadata."
self:NotifyPlayer( ply, "[Request Error] " .. err )
return
end
media:SetOwner( ply )
if not self:ShouldAddMedia(media) then
return
end
-- Add the media to the queue
self:AddMedia( media )
local msg = string.format( "Added '%s' to the queue", media:Title() )
self:NotifyPlayer( ply, msg )
self:BroadcastUpdate()
MediaPlayer.History:LogRequest( media )
hook.Run( "PostMediaPlayerMediaRequest", self, media, ply )
end)
end
function MEDIAPLAYER:RequestPause( ply )
-- Player must be valid and also a listener
if not ( IsValid(ply) and self:HasListener(ply) ) then
return
end
-- Check player priviledges
if not self:IsPlayerPrivileged(ply) then
return
end
if MediaPlayer.DEBUG then
print( "MEDIAPLAYER.RequestPause", ply )
end
-- Toggle player state
if self:GetPlayerState() == MP_STATE_PLAYING then
self:SetPlayerState( MP_STATE_PAUSED )
else
self:SetPlayerState( MP_STATE_PLAYING )
end
net.Start( "MEDIAPLAYER.Pause" )
net.WriteString( self:GetId() )
self.net.WritePlayerState( self:GetPlayerState() )
net.Send( self._Listeners )
end
function MEDIAPLAYER:RequestSkip( ply )
if not (self:GetPlayerState() >= MP_STATE_PLAYING) then return end
-- Player must be valid and also a listener
if not ( IsValid(ply) and self:HasListener(ply) ) then
return
end
-- Check player priviledges
if not self:IsPlayerPrivileged(ply) then
return
end
if MediaPlayer.DEBUG then
print( "MEDIAPLAYER.RequestSkip", ply )
end
self:NextMedia()
end
local function ParseTime( time )
local tbl = {}
-- insert fragments in reverse
for fragment, _ in string.gmatch(time, ":?(%d+)") do
table.insert(tbl, 1, tonumber(fragment) or 0)
end
if #tbl == 0 then
return nil
end
local seconds = 0
for i = 1, #tbl do
seconds = seconds + tbl[i] * math.max(60 ^ (i-1), 1)
end
return seconds
end
function MEDIAPLAYER:RequestSeek( ply, seekTime )
if not (self:GetPlayerState() >= MP_STATE_PLAYING) then return end
-- Player must be valid and also a listener
if not ( IsValid(ply) and self:HasListener(ply) ) then
return
end
-- Check player priviledges
if not self:IsPlayerPrivileged(ply) then
return
end
if MediaPlayer.DEBUG then
print( "MEDIAPLAYER.RequestSeek", ply, seekTime )
end
local media = self:CurrentMedia()
-- Ignore requests for media that isn't timed
if not media:IsTimed() then
return
end
-- Convert HH:MM:SS to seconds
local seconds = ParseTime( seekTime )
if not seconds then return end
-- Ignore request if time is past the end of the video
if seconds > media:Duration() then
self:NotifyPlayer( ply, "Request seek time was past the end of the media duration" )
return
end
-- NOTE: this can result in a negative number if the server was recently
-- started.
local startTime = CurTime() - seconds
media:StartTime( startTime )
self:UpdateListeners()
net.Start( "MEDIAPLAYER.Seek" )
net.WriteString( self:GetId() )
net.WriteInt( startTime, 32 )
net.Send( self._Listeners )
end
--[[---------------------------------------------------------
Media Player Updates
-----------------------------------------------------------]]
function MEDIAPLAYER:BroadcastUpdate( ply )
if MediaPlayer.DEBUG then
print( "MEDIAPLAYER.BroadcastUpdate", ply )
end
self:UpdateListeners()
net.Start( "MEDIAPLAYER.Update" )
net.WriteString( self:GetId() )
net.WriteString( self.Name )
self.net.WritePlayerState( self:GetPlayerState() )
self:NetWriteUpdate()
net.WriteUInt( #self._Queue, math.CeilPower2(self.MaxMediaItems)/2 )
for _, media in pairs(self._Queue) do
self.net.WriteMedia(media)
end
net.Send( ply or self._Listeners )
end
function MEDIAPLAYER:NetWriteUpdate()
-- Allows for another media player type to extend update net messages
end
-- Player requesting queue update
net.Receive( "MEDIAPLAYER.Update", function(len, ply)
local id = net.ReadString()
local mp = MediaPlayer.GetById(id)
if not mp then return end
-- TODO: prevent request spam
mp:BroadcastUpdate(ply)
end )
function MEDIAPLAYER:NotifyPlayer( ply, msg )
ply:ChatPrint( msg )
end

View File

@ -0,0 +1,367 @@
local MediaPlayer = MediaPlayer
--[[---------------------------------------------------------
Base Media Player
-----------------------------------------------------------]]
local MEDIAPLAYER = MEDIAPLAYER
MEDIAPLAYER.__index = MEDIAPLAYER
-- Inherit EventEmitter for all mediaplayer instances
EventEmitter:new(MEDIAPLAYER)
MEDIAPLAYER.Name = "base"
MEDIAPLAYER.IsMediaPlayer = true
MEDIAPLAYER.MaxMediaItems = 64
MEDIAPLAYER.NoMedia = "\4" -- end of transmission character
-- Media Player states
MP_STATE_ENDED = 0
MP_STATE_PLAYING = 1
MP_STATE_PAUSED = 2
NUM_MP_STATE = 3
--
-- Initialize the media player object.
--
function MEDIAPLAYER:Init(params)
self._Queue = {} -- media queue
self._Media = nil -- current media
self._Owner = nil -- theater owner
self._State = MP_STATE_ENDED -- waiting for new media
if SERVER then
self._TransmitState = TRANSMIT_ALWAYS
self._Listeners = {}
self._Location = -1
else
self._LastMediaUpdate = 0
control.Add( KEY_Q, self, self.OnQueueKeyPressed )
control.Add( KEY_C, self, self.OnQueueKeyPressed )
end
-- Merge in any passed in params
-- table.Merge(self, params or {})
end
--
-- Get whether the media player is valid.
--
-- @return boolean Whether the media player is valid
--
function MEDIAPLAYER:IsValid()
return true
end
--
-- String coercion metamethod
--
-- @return String Media player string representation
--
function MEDIAPLAYER:__tostring()
return string.join( ', ',
self:GetId() )
end
--
-- Get the media player's unique ID.
--
-- @return Number Media player ID.
--
function MEDIAPLAYER:GetId()
return self.id
end
--
-- Get the media player's type.
--
-- @return String MP type.
--
function MEDIAPLAYER:GetType()
return self.Name
end
function MEDIAPLAYER:GetPlayerState()
return self._State
end
function MEDIAPLAYER:SetPlayerState( state )
local current = self._State
self._State = state
if MediaPlayer.DEBUG then
print( "MEDIAPLAYER.SetPlayerState", state )
end
if current ~= state then
self:OnPlayerStateChanged( current, state )
end
end
function MEDIAPLAYER:OnPlayerStateChanged( old, new )
local media = self:GetMedia()
local validMedia = IsValid(media)
if MediaPlayer.DEBUG then
print( "MEDIAPLAYER.OnPlayerStateChanged", old .. ' => ' .. new )
end
if new == MP_STATE_PLAYING then
if validMedia and not media:IsPlaying() then
media:Play()
end
elseif new == MP_STATE_PAUSED then
if validMedia and media:IsPlaying() then
media:Pause()
end
end
end
--
-- Get whether the media player is currently playing media.
--
-- @return boolean Media is playing
--
function MEDIAPLAYER:IsPlaying()
return self._State == MP_STATE_PLAYING
end
--
-- Get the media player's position.
--
-- @return Vector3 Media player's position
--
function MEDIAPLAYER:GetPos()
if not self._pos then
self._pos = Vector(0,0,0)
end
return self._pos
end
--
-- Get the media player's location ID.
--
-- @return Number Media player's location ID
--
function MEDIAPLAYER:GetLocation()
return self._Location
end
function MEDIAPLAYER:GetOwner()
return self._Owner
end
function MEDIAPLAYER:SetOwner( ply )
self._Owner = ply
end
--
-- Media player update
--
function MEDIAPLAYER:Think()
if SERVER then
self:UpdateListeners()
end
local media = self:GetMedia()
local validMedia = IsValid(media)
-- Waiting to play new media
if self._State <= MP_STATE_ENDED then
-- Check queue for videos to play
-- TODO: perform state change when media is added
if not self:IsQueueEmpty() then
self:OnMediaFinished()
end
elseif self._State == MP_STATE_PLAYING then
-- Wait for media to finish
if validMedia and media:IsTimed() then
local time = media:CurrentTime()
local duration = media:Duration()
if time > duration then
self:OnMediaFinished()
end
end
end
if CLIENT and validMedia then
media:Sync()
-- TODO: check if volume has changed first?
media:Volume( MediaPlayer.Volume() )
end
end
--
-- Get the currently playing media.
--
-- @return Media Currently playing media
--
function MEDIAPLAYER:GetMedia()
return self._Media
end
MEDIAPLAYER.CurrentMedia = MEDIAPLAYER.GetMedia
--
-- Set the currently playing media.
--
-- @param media Media object.
--
function MEDIAPLAYER:SetMedia( media )
self._Media = media
self:OnMediaStarted( media )
-- NOTE: media can be nil!
self:emit('mediaChanged', media)
end
--
-- Get the media queue.
-- TODO: Remove this as it should only be accessed internally?
--
-- @return table Media queue.
--
function MEDIAPLAYER:GetMediaQueue()
return self._Queue
end
--
-- Clear the media queue.
--
function MEDIAPLAYER:ClearMediaQueue()
self._Queue = {}
if SERVER then
self:BroadcastUpdate()
end
end
--
-- Get whether the media queue is empty.
--
-- @return boolean Whether the queue is empty
--
function MEDIAPLAYER:IsQueueEmpty()
return #self._Queue == 0
end
--
-- Add media to the queue.
--
-- @param media Media object.
--
function MEDIAPLAYER:AddMedia( media )
if not media then return end
if SERVER then
-- Add an extra second for client buffering time
media:Duration( media:Duration() + 1 )
end
table.insert( self._Queue, media )
end
--
-- Event called when media should begin playing.
--
-- @param media Media object to be played.
--
function MEDIAPLAYER:OnMediaStarted( media )
media = media or self:CurrentMedia()
if MediaPlayer.DEBUG then
print( "MEDIAPLAYER.OnMediaStarted", media )
end
if IsValid(media) then
if SERVER then
media:StartTime( CurTime() + 1 )
else
self._LastMediaUpdate = RealTime()
end
if SERVER then
self:SetPlayerState( MP_STATE_PLAYING )
end
self:emit('mediaStarted', media)
elseif SERVER then
self:SetPlayerState( MP_STATE_ENDED )
end
end
--
-- Event called when media should stop playing and the next in the queue
-- should begin.
--
-- @param media Media object to stop.
--
function MEDIAPLAYER:OnMediaFinished( media )
media = media or self:CurrentMedia()
if MediaPlayer.DEBUG then
print( "MEDIAPLAYER.OnMediaFinished", media )
end
if SERVER then
self:SetPlayerState( MP_STATE_ENDED )
end
self._Media = nil
if CLIENT and IsValid(media) then
-- TODO: Reuse browser if it was the same video type
media:Stop()
end
self:emit('mediaFinished', media)
if SERVER then
self:NextMedia()
end
end
--
-- Event called when the media player is to be removed/destroyed.
--
function MEDIAPLAYER:Remove()
MediaPlayer.Destroy( self )
if SERVER then
-- Remove all listeners
for _, ply in pairs( self._Listeners ) do
-- TODO: it's probably better not to send individual net messages
-- for each player removed.
self:RemoveListener( ply )
end
else
local media = self:CurrentMedia()
if IsValid(media) then
media:Stop()
end
end
end

View File

@ -0,0 +1,27 @@
local net = net
local EOT = "\4" -- End of transmission
MEDIAPLAYER.net = {}
local mpnet = MEDIAPLAYER.net
function mpnet.WriteDuration( seconds )
net.WriteUInt( seconds, 16 )
end
function mpnet.WriteMedia( media )
if media then
net.WriteString( media:Url() )
net.WriteString( media:Title() )
mpnet.WriteDuration( media:Duration() )
net.WriteString( media:OwnerName() )
net.WriteString( media:OwnerSteamID() )
else
net.WriteString( EOT )
end
end
local StateBits = math.CeilPower2(NUM_MP_STATE) / 2
function mpnet.WritePlayerState( state )
net.WriteUInt(state, StateBits)
end

View File

@ -0,0 +1,106 @@
include "shared.lua"
DEFINE_BASECLASS( "mp_base" )
local pcall = pcall
local print = print
local Angle = Angle
local IsValid = IsValid
local ValidPanel = ValidPanel
local Vector = Vector
local cam = cam
local Start3D = cam.Start3D
local Start3D2D = cam.Start3D2D
local End3D2D = cam.End3D2D
local draw = draw
local math = math
local string = string
local surface = surface
local FullscreenCvar = MediaPlayer.Cvars.Fullscreen
MEDIAPLAYER.Enable3DAudio = true
function MEDIAPLAYER:NetReadUpdate()
local entIndex = net.ReadUInt(16)
local ent = Entity(entIndex)
local mpEnt = self.Entity
if MediaPlayer.DEBUG then
print("MEDIAPLAYER.NetReadUpdate[entity]: ", ent, entIndex)
end
if ent ~= mpEnt then
if IsValid(ent) and ent ~= NULL then
ent:InstallMediaPlayer( self )
else
-- Wait until the entity becomes valid
self._EntIndex = entIndex
end
end
end
local RenderScale = 0.1
local InfoScale = 1/17
function MEDIAPLAYER:Draw( bDrawingDepth, bDrawingSkybox )
local ent = self.Entity
if --bDrawingSkybox or
FullscreenCvar:GetBool() or -- Don't draw if we're drawing fullscreen
not IsValid(ent) or
(ent.IsDormant and ent:IsDormant()) then
return
end
local media = self:CurrentMedia()
-- TODO: Draw thumbnail at far distances?
local w, h, pos, ang = ent:GetMediaPlayerPosition()
-- Render scale
local rw, rh = w / RenderScale, h / RenderScale
if IsValid(media) then
-- Custom media draw function
if media.Draw then
Start3D2D( pos, ang, RenderScale )
media:Draw( rw, rh )
End3D2D()
end
-- TODO: else draw 'not yet implemented' screen?
-- Media info
Start3D2D( pos, ang, InfoScale )
local iw, ih = w / InfoScale, h / InfoScale
local succ, err = pcall( self.DrawMediaInfo, self, media, iw, ih )
if not succ then
print( err )
end
End3D2D()
else
local browser = MediaPlayer.GetIdlescreen()
if ValidPanel(browser) then
Start3D2D( pos, ang, RenderScale )
self:DrawHTML( browser, rw, rh )
End3D2D()
end
end
end
function MEDIAPLAYER:SetMedia( media )
if media and self.Enable3DAudio then
-- Set entity on media for 3D support
media.Entity = self:GetEntity()
end
BaseClass.SetMedia( self, media )
end

View File

@ -0,0 +1,10 @@
AddCSLuaFile "shared.lua"
AddCSLuaFile "sh_meta.lua"
include "shared.lua"
function MEDIAPLAYER:NetWriteUpdate()
-- Write the entity index since the actual entity may not yet exist on a
-- client that's not fully connected.
local entIndex = IsValid(self.Entity) and self.Entity:EntIndex() or 0
net.WriteUInt(entIndex, 16)
end

View File

@ -0,0 +1,85 @@
--[[---------------------------------------------------------
Media Player Entity Meta
-----------------------------------------------------------]]
local EntityMeta = FindMetaTable("Entity")
if not EntityMeta then return end
function EntityMeta:GetMediaPlayer()
return self._mp
end
--
-- Installs a media player reference to the entity.
--
-- @param Table|String mp Media player table or string type.
function EntityMeta:InstallMediaPlayer( mp )
if not istable(mp) then
local mpType = isstring(mp) and mp or "entity"
if not MediaPlayer.IsValidType(mpType) then
ErrorNoHalt("ERROR: Attempted to install invalid mediaplayer type onto an entity!\n")
ErrorNoHalt("ENTITY: " .. tostring(self) .. "\n")
ErrorNoHalt("TYPE: " .. tostring(mpType) .. "\n")
mpType = "entity" -- default
end
local mpId = "Entity" .. self:EntIndex()
mp = MediaPlayer.Create( mpId, mpType )
end
self._mp = mp
self._mp:SetEntity(self)
if isfunction(self.SetupMediaPlayer) then
self:SetupMediaPlayer(mp)
end
end
local DefaultConfig = {
offset = Vector(0,0,0), -- translation from entity origin
angle = Angle(0,0,0), -- rotation
-- attachment = "corner" -- attachment name
width = 64, -- screen width
height = 64 * 9/16 -- screen height
}
function EntityMeta:GetMediaPlayerPosition()
local cfg = self.PlayerConfig or DefaultConfig
local w = (cfg.width or DefaultConfig.width)
local h = (cfg.height or DefaultConfig.height)
local pos, ang
if cfg.attachment then
local idx = self:LookupAttachment(cfg.attachment)
if not idx then
local err = string.format("MediaPlayer:Entity.Draw: Invalid attachment '%s'\n", cfg.attachment)
Error(err)
end
-- Get attachment orientation
local attach = self:GetAttachment(idx)
pos = attach.pos
ang = attach.ang
else
pos = self:GetPos() -- TODO: use GetRenderOrigin?
end
-- Apply offset
if cfg.offset then
pos = pos +
self:GetForward() * cfg.offset.x +
self:GetRight() * cfg.offset.y +
self:GetUp() * cfg.offset.z
end
-- Set angles
ang = ang or self:GetAngles() -- TODO: use GetRenderAngles?
ang:RotateAroundAxis( ang:Right(), cfg.angle.p )
ang:RotateAroundAxis( ang:Up(), cfg.angle.y )
ang:RotateAroundAxis( ang:Forward(), cfg.angle.r )
return w, h, pos, ang
end

View File

@ -0,0 +1,89 @@
include "sh_meta.lua"
DEFINE_BASECLASS( "mp_base" )
--[[---------------------------------------------------------
Entity Media Player
-----------------------------------------------------------]]
local MEDIAPLAYER = MEDIAPLAYER
MEDIAPLAYER.Name = "entity"
function MEDIAPLAYER:IsValid()
return self.Entity and IsValid(self.Entity)
end
function MEDIAPLAYER:Init(...)
BaseClass.Init(self, ...)
if SERVER then
-- Manually manage listeners by default
self._TransmitState = TRANSMIT_NEVER
else--if CLIENT and self.Render then
hook.Add( "HUDPaint", self, self.DrawFullscreen )
hook.Add( "PostDrawOpaqueRenderables", self, self.Draw )
end
end
function MEDIAPLAYER:SetEntity(ent)
self.Entity = ent
end
function MEDIAPLAYER:GetEntity()
-- Clients may wait for the entity to become valid
if CLIENT and self._EntIndex then
local ent = Entity(self._EntIndex)
if IsValid(ent) and ent ~= NULL then
ent:InstallMediaPlayer(self)
self._EntIndex = nil
else
return nil
end
end
return self.Entity
end
function MEDIAPLAYER:GetPos()
return IsValid(self.Entity) and self.Entity:GetPos() or Vector(0,0,0)
end
function MEDIAPLAYER:GetLocation()
if IsValid(self.Entity) and self.Entity.Location then
return self.Entity:Location()
end
return self._Location
end
function MEDIAPLAYER:Think()
BaseClass.Think(self)
local ent = self:GetEntity()
if IsValid(ent) then
-- Lua refresh fix
if ent._mp ~= self then
self:Remove()
end
elseif SERVER then
-- Only remove on the server since the client may still be connecting
-- and the entity will be created when they finish.
self:Remove()
end
end
function MEDIAPLAYER:Remove()
-- remove draw hooks
if CLIENT then
hook.Remove( "HUDPaint", self )
hook.Remove( "PostDrawOpaqueRenderables", self )
end
-- remove reference to media player installed on entity
if self.Entity then
self.Entity._mp = nil
end
BaseClass.Remove(self)
end

View File

@ -0,0 +1,297 @@
include "shared.lua"
DEFINE_BASECLASS( "mp_service_base" )
-- http://www.un4seen.com/doc/#bass/BASS_StreamCreateURL.html
SERVICE.StreamOptions = { "noplay", "noblock" }
function SERVICE:Volume( volume )
volume = BaseClass.Volume( self, volume )
if IsValid(self.Channel) then
local vol = volume > 1 and volume/100 or volume
-- IGModAudioChannel is limited by the actual gmod volume
local gmvolume = GetConVarNumber("volume")
if gmvolume > vol then
vol = vol / gmvolume
else
vol = 1
end
self.Channel:SetVolume( vol )
end
return volume
end
function SERVICE:Play()
BaseClass.Play( self )
if IsValid(self.Channel) then
self.Channel:Play()
else
local settings = table.Copy(self.StreamOptions)
-- .ogg files can't seem to use 3d?
if IsValid(self.Entity) and not self.url:match(".ogg") then
table.insert(settings, "3d")
end
settings = table.concat(settings, " ")
sound.PlayURL( self.url, settings, function( channel )
if IsValid(channel) then
self.Channel = channel
-- The song may have been skipped before the channel was
-- created, only play if the media state is set to play.
if self:IsPlaying() then
self:Volume()
self:Sync()
self.Channel:Play()
end
self:emit('channelReady', channel)
else
ErrorNoHalt( "There was a problem playing the audio file: " .. self.url .. "\n" )
end
end )
end
end
function SERVICE:Pause()
BaseClass.Pause(self)
if IsValid(self.Channel) then
self.Channel:Pause()
end
end
function SERVICE:Stop()
BaseClass.Stop(self)
if IsValid(self.Channel) then
self.Channel:Stop()
end
end
function SERVICE:Sync()
if self:IsPlaying() and IsValid(self.Channel) then
if self:IsTimed() then
self:SyncTime()
end
self:SyncEntityPos()
end
end
function SERVICE:SyncTime()
local state = self.Channel:GetState()
if state ~= GMOD_CHANNEL_STALLED then
local duration = self.Channel:GetLength()
local seekTime = math.min(duration, self:CurrentTime())
local curTime = self.Channel:GetTime()
local diffTime = math.abs(curTime - seekTime)
if diffTime > 5 then
self.Channel:SetTime( seekTime )
end
end
end
function SERVICE:SyncEntityPos()
if IsValid(self.Entity) then
if self.Channel:Is3D() then
-- apparently these are the default values?
self.Channel:Set3DFadeDistance( 500, 1000 )
self.Channel:SetPos( self.Entity:GetPos() )
else
-- TODO: Fake 3D volume
-- http://facepunch.com/showthread.php?t=1302124&p=41975238&viewfull=1#post41975238
-- local volume = BaseClass.Volume( self, volume )
-- local vol = volume > 1 and volume/100 or volume
-- self.Channel:SetVolume( vol )
end
end
end
function SERVICE:PreRequest( callback )
-- LocalPlayer():ChatPrint( "Prefetching data for '" .. self.url .. "'..." )
sound.PlayURL( self.url, "noplay", function( channel )
if MediaPlayer.DEBUG then
print("AUDIOFILE.PreRequest", channel)
end
if IsValid(channel) then
-- Set metadata to later send to the server; IGModAudioChannel is
-- only accessible on the client.
self._metadata = {}
self._metadata.title = channel:GetFileName()
self._metadata.duration = channel:GetLength()
-- TODO: limit the duration in some way so a client doesn't try to
-- spoof this
callback()
channel:Stop()
else
callback("There was a problem prefetching audio metadata.")
end
end )
end
function SERVICE:NetWriteRequest()
net.WriteString( self:Title() )
net.WriteUInt( self:Duration(), 16 )
end
--[[---------------------------------------------------------
Draw 3D2D
-----------------------------------------------------------]]
local IsValid = IsValid
local draw = draw
local math = math
local surface = surface
local VisualizerBgColor = Color(44, 62, 80, 255)
local VisualizerBarColor = Color(52, 152, 219)
local BandGridHeight = 16
local BandGridWidth = math.ceil( BandGridHeight * 16/9 )
local NumBands = 256
local BandStepSize = math.floor(NumBands / BandGridWidth)
local BarPadding = 1
-- local BandGrid = {}
-- for x = 1, BandGridWidth do
-- BandGrid[x] = {}
-- for y = 1, BandGridHeight do
-- BandGrid[x][y] =
-- end
-- end
-- http://inchoatethoughts.com/a-wpf-spectrum-analyzer-for-audio-visualization
-- http://wpfsvl.codeplex.com/SourceControl/latest#WPFSoundVisualizationLib/Main/Source/WPFSoundVisualizationLibrary/Spectrum Analyzer/SpectrumAnalyzer.cs
--[[function MEDIAPLAYER:DrawSpectrumAnalyzer( media, w, h )
local channel = media.Channel
if channel:GetState() ~= GMOD_CHANNEL_PLAYING then
return
end
local fft = {}
channel:FFT( fft, FFT_512 )
surface.SetDrawColor(VisualizerBarColor)
local BarWidth = math.floor(w / BandGridWidth)
local b0 = 1
for x = 0, BandGridWidth do
local sum = 0
local sc = 0
local b1 = math.pow(2, x * 10.0 / BandGridWidth)
if (b1 > NumBands) then b1 = NumBands end
if (b1<=b0) then b1 = b0 end
sc=10+b1-b0
while b0 < b1 do
sum = sum + fft[b0]
b0 = b0 + 1
end
local BarHeight = math.floor(math.sqrt(sum/math.log10(sc)) * 1.7 * h)
BarHeight = math.Clamp(BarHeight, 0, h)
surface.DrawRect(
(x * BarWidth) + BarPadding,
h - BarHeight,
BarWidth - (BarPadding * 2),
BarHeight
)
end
end]]
local BANDS = 28
function DrawSpectrumAnalyzer( channel, w, h )
-- Background
surface.SetDrawColor( VisualizerBgColor )
surface.DrawRect( 0, 0, w, h )
if channel:GetState() ~= GMOD_CHANNEL_PLAYING then
return
end
local fft = {}
channel:FFT( fft, FFT_2048 )
local b0 = 1
-- surface.SetDrawColor(VisualizerBarColor)
local x, y
for x = 0, BANDS do
local sum = 0
local sc = 0
local b1 = math.pow(2,x*10.0/(BANDS-1))
if (b1>1023) then b1=1023 end
if (b1<=b0) then b1=b0+1 end
sc=10+b1-b0;
while b0 < b1 do
sum = sum + fft[b0]
b0 = b0 + 1
end
y = (math.sqrt(sum/math.log10(sc))*1.7*h)-4
y = math.Clamp(y, 0, h)
local col = HSVToColor( 120 - (120 * y/h), 1, 1 )
surface.SetDrawColor(col)
surface.DrawRect(
math.ceil(x*(w/BANDS)),
math.ceil(h - y - 1),
math.ceil(w/BANDS) - 2,
y + 1
)
end
end
function SERVICE:Draw( w, h )
if IsValid(self.Channel) then
DrawSpectrumAnalyzer( self.Channel, w, h )
end
end

View File

@ -0,0 +1,112 @@
AddCSLuaFile "shared.lua"
include "shared.lua"
local urllib = url
local FilenamePattern = "([^/]+)%.%w-$"
local function titleFallback(self, callback)
local path = self.urlinfo.path
path = string.match( path, FilenamePattern ) -- get filename
title = urllib.unescape( path )
self._metadata.title = title
self:SetMetadata(self._metadata, true)
MediaPlayer.Metadata:Save(self)
callback(self._metadata)
end
--[[local function id3(self, callback)
self:Fetch( self.url,
function(body, len, headers, code)
local title, artist
-- check header
if body:sub(1, 4) == "TAG+" then
title = body:sub(5, 56)
artist = body:sub(57, 116)
elseif body:sub(1, 3) == "TAG" then
title = body:sub(4, 33)
artist = body:sub(34, 63)
else
titleFallback(self, callback)
return
end
title = title:Trim()
artist = artist:Trim()
print("ID3 SUCCESS:", title, artist)
if artist:len() > 0 then
title = artist .. ' - ' .. title
end
self._metadata.title = title
callback(self._metadata)
end,
function()
titleFallback(self, callback)
end,
{
["Range"] = "bytes=-128"
}
)
end]]
function SERVICE:GetMetadata( callback )
local ext = self:GetExtension()
-- if ext == 'mp3' then
-- id3(self, callback)
-- else
if not self._metadata then
self._metadata = {
title = "Unknown audio",
duration = 0
}
end
if callback then
self:SetMetadata(self._metadata, true)
MediaPlayer.Metadata:Save(self)
callback(self._metadata)
end
-- end
end
function SERVICE:GetExtension()
if not self._extension then
self._extension = string.GetExtensionFromFilename(self.url)
end
return self._extension
end
function SERVICE:NetReadRequest()
if not self.PrefetchMetadata then return end
local title = net.ReadString()
-- If the title is just the URL, grab just the filename instead
if title == self.url then
local path = self.urlinfo.path
path = string.match( path, FilenamePattern ) -- get filename
title = urllib.unescape( path )
end
self._metadata = self._metadata or {}
self._metadata.title = title
self._metadata.duration = net.ReadUInt( 16 )
end

View File

@ -0,0 +1,22 @@
local urllib = url
SERVICE.Name = "Audio file"
SERVICE.Id = "af"
SERVICE.PrefetchMetadata = true
local SupportedEncodings = {
'([^/]+%.[mM][pP]3)$', -- mp3
'([^/]+%.[wW][aA][vV])$', -- wav
'([^/]+%.[oO][gG][gG])$' -- ogg
}
function SERVICE:Match( url )
-- check supported encodings
for _, encoding in pairs(SupportedEncodings) do
if url:find(encoding) then
return true
end
end
return false
end

View File

@ -0,0 +1,33 @@
include "shared.lua"
function SERVICE:Volume( volume )
if volume then
self._volume = tonumber(volume) or self._volume
end
return self._volume
end
function SERVICE:IsPaused()
return self._PauseTime ~= nil
end
function SERVICE:Stop()
self._playing = false
self:emit('stop')
end
function SERVICE:PlayPause()
if self:IsPlaying() then
self:Pause()
else
self:Play()
end
end
function SERVICE:Sync()
-- Implement this in timed services
end
function SERVICE:NetWriteRequest()
-- Send any additional net data here
end

View File

@ -0,0 +1,102 @@
AddCSLuaFile "shared.lua"
include "shared.lua"
local MaxTitleLength = 128
function SERVICE:SetOwner( ply )
self._Owner = ply
self._OwnerName = ply:Nick()
self._OwnerSteamID = ply:SteamID()
end
function SERVICE:SetMetadata( metadata, new )
self._metadata = metadata
if new then
local title = self._metadata.title or "Unknown"
title = title:sub(1, MaxTitleLength)
-- Escape any '%' char with a letter following it
title = title:gsub('%%%a', '%%%%')
self._metadata.title = title
end
end
function SERVICE:GetMetadata( callback )
if not self._metadata then
self._metadata = {
title = "Base service",
duration = -1,
url = "",
thumbnail = ""
}
end
callback(self._metadata)
end
local HttpHeaders = {
["Cache-Control"] = "no-cache",
["Connection"] = "keep-alive",
-- Required for Google API requests; uses browser API key.
["Referer"] = MediaPlayer.GetConfigValue('google.referrer'),
-- Don't use improperly formatted GMod user agent in case anything actually
-- checks the user agent.
["User-Agent"] = "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/35.0.1916.114 Safari/537.36"
}
function SERVICE:Fetch( url, onReceive, onFailure, headers )
if MediaPlayer.DEBUG then
print( "SERVICE.Fetch", url )
end
local request = {
url = url,
method = "GET",
success = function( code, body, headers )
if MediaPlayer.DEBUG then
print("HTTP Results["..code.."]:", url)
PrintTable(headers)
print(body)
end
if isfunction(onReceive) then
onReceive( body, body:len(), headers, code )
end
end,
failed = function( err )
if isfunction(onFailure) then
onFailure( err )
end
end
}
-- Pass in extra headers
if headers then
local tbl = table.Copy( HttpHeaders )
table.Merge( tbl, headers )
request.headers = tbl
else
request.headers = HttpHeaders
end
if MediaPlayer.DEBUG then
print "MediaPlayer.Service.Fetch REQUESTING"
PrintTable(request)
end
HTTP(request)
end
function SERVICE:NetReadRequest()
-- Read any additional net data here
end

View File

@ -0,0 +1,191 @@
local string = string
local urllib = url
local CurTime = CurTime
SERVICE.Name = "Base Service"
SERVICE.Id = "base"
SERVICE.Abstract = true
-- Inherit EventEmitter for all service instances
EventEmitter:new(SERVICE)
local OwnerInfoPattern = "%s [%s]"
function SERVICE:New( url )
local obj = setmetatable( {}, {
__index = self,
__tostring = self.__tostring
} )
obj.url = url
local success, urlinfo = pcall(urllib.parse2, url)
obj.urlinfo = success and urlinfo or {}
if CLIENT then
obj._playing = false
obj._volume = 0.33
end
return obj
end
function SERVICE:__tostring()
return string.join( ', ',
self:Title(),
string.FormatSeconds(self:Duration()),
self:OwnerName() )
end
--
-- Determines if the media is valid.
--
-- @return boolean
--
function SERVICE:IsValid()
return true
end
--
-- Determines if the media supports the given URL.
--
-- @param url URL.
-- @return boolean
--
function SERVICE:Match( url )
return false
end
--
-- Gives the unique data used as part of the primary key in the metadata
-- database.
--
-- @return String
--
function SERVICE:Data()
return self._data
end
function SERVICE:Owner()
return self._Owner
end
SERVICE.GetOwner = SERVICE.Owner
function SERVICE:OwnerName()
return self._OwnerName
end
function SERVICE:OwnerSteamID()
return self._OwnerSteamID
end
function SERVICE:OwnerInfo()
return OwnerInfoPattern:format( self._OwnerName, self._OwnerSteamID )
end
function SERVICE:Title()
return self._metadata and self._metadata.title or "Unknown"
end
function SERVICE:Duration( duration )
if duration then
self._metadata = self._metadata or {}
self._metadata.duration = duration
end
return self._metadata and self._metadata.duration or -1
end
--
-- Determines whether the media is timed.
--
-- @return boolean
--
function SERVICE:IsTimed()
return true
end
function SERVICE:Thumbnail()
return self._metadata and self._metadata.thumbnail
end
function SERVICE:Url()
return self.url
end
SERVICE.URL = SERVICE.Url
function SERVICE:UniqueID()
if not self._id then
local data = self:Data()
if not data then
data = util.CRC(self.url)
end
-- e.g. yt-G2MORmw703o
self._id = string.format( "%s-%s", self.Id, data )
end
return self._id
end
--[[----------------------------------------------------------------------------
Playback
------------------------------------------------------------------------------]]
function SERVICE:StartTime( seconds )
if type(seconds) == 'number' then
if self._PauseTime then
self._PauseTime = CurTime()
end
self._StartTime = seconds
end
if self._PauseTime then
local diff = self._PauseTime - self._StartTime
return CurTime() - diff
else
return self._StartTime
end
end
function SERVICE:CurrentTime()
if self._StartTime then
if self._PauseTime then
return self._PauseTime - self._StartTime
else
return CurTime() - self._StartTime
end
else
return -1
end
end
function SERVICE:IsPlaying()
return self._playing
end
function SERVICE:Play()
if self._PauseTime then
-- Update start time to match the time when paused
self._StartTime = CurTime() - (self._PauseTime - self._StartTime)
self._PauseTime = nil
end
self._playing = true
if CLIENT then
self:emit('play')
end
end
function SERVICE:Pause()
self._PauseTime = CurTime()
self._playing = false
if CLIENT then
self:emit('pause')
end
end

View File

@ -0,0 +1,130 @@
DEFINE_BASECLASS( "mp_service_base" )
SERVICE.Name = "Browser Base"
SERVICE.Id = "browser"
SERVICE.Abstract = true
if CLIENT then
function SERVICE:GetBrowser()
return self.Browser
end
function SERVICE:OnBrowserReady( browser )
-- TODO: make sure this resolution is correct
local resolution = MediaPlayer.Resolution()
MediaPlayer.SetBrowserSize( browser, resolution * 16/9, resolution )
-- Implement this in a child service
end
function SERVICE:SetVolume( volume )
-- Implement this in a child service
end
function SERVICE:Volume( volume )
local origVolume = volume
volume = BaseClass.Volume( self, volume )
if origVolume and ValidPanel( self.Browser ) then
self:SetVolume( volume )
end
return volume
end
function SERVICE:Play()
BaseClass.Play( self )
if self.Browser and ValidPanel(self.Browser) then
self:OnBrowserReady( self.Browser )
else
self._promise = browserpool.get(function( panel )
if not panel then
return
end
if self._promise then
self._promise = nil
end
self.Browser = panel
self:OnBrowserReady( panel )
end)
end
end
function SERVICE:Stop()
BaseClass.Stop( self )
if self._promise then
self._promise:Cancel('Service has been stopped')
end
if self.Browser then
browserpool.release( self.Browser )
self.Browser = nil
end
end
local StartHtml = [[
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Media Player</title>
<style type="text/css">
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
}
* { box-sizing: border-box }
body {
background-color: #282828;
color: #cecece;
}
</style>
</head>
<body>
]]
local EndHtml = [[
</body>
</html>
]]
function SERVICE.WrapHTML( html )
return table.concat({ StartHtml, html, EndHtml })
end
--[[---------------------------------------------------------
Draw 3D2D
-----------------------------------------------------------]]
local ValidPanel = ValidPanel
local SetDrawColor = surface.SetDrawColor
local DrawRect = surface.DrawRect
local HTMLTexture = draw.HTMLTexture
function SERVICE:Draw( w, h )
if ValidPanel(self.Browser) then
SetDrawColor( 0, 0, 0, 255 )
DrawRect( 0, 0, w, h )
HTMLTexture( self.Browser, w, h )
end
end
end

View File

@ -0,0 +1,31 @@
include "shared.lua"
DEFINE_BASECLASS( "mp_service_browser" )
-- data:text/html,<object type="application/x-shockwave-flash" allowscriptaccess="always" allowfullscreen="true" wmode="opaque" data="https://video.google.com/get_player?docid=0B1K_ByAqaFKGamdrajd6WXFUSEs0VHI4eTJHNHpPdw&partnerid=30&el=leaf&cc_load_policy=1&enablejsapi=1&autoplay=1&start=30" width="100%" height="100%" style="visibility: visible;"></object>
-- https://docs.google.com/file/d/0B1K_ByAqaFKGamdrajd6WXFUSEs0VHI4eTJHNHpPdw/preview
local EmbedHtml = [[
<object type="application/x-shockwave-flash" allowscriptaccess="always" allowfullscreen="true" wmode="opaque" data="%s" width="100%%" height="100%%" style="visibility: visible;"></object>]]
SERVICE.VideoUrlFormat = "https://video.google.com/get_player?docid=%s&enablejsapi=1&autoplay=1&controls=0&modestbranding=1&rel=0&showinfo=0&wmode=opaque&ps=docs&partnerid=30"
function SERVICE:OnBrowserReady( browser )
BaseClass.OnBrowserReady( self, browser )
local fileId = self:GetGoogleDriveFileId()
local url = self.VideoUrlFormat:format(fileId)
local curTime = self:CurrentTime()
-- Add start time to URL if the video didn't just begin
if self:IsTimed() and curTime > 3 then
url = url .. "&start=" .. math.Round(curTime)
end
local html = self.WrapHTML( EmbedHtml:format(url) )
browser:SetHTML( html )
end

View File

@ -0,0 +1,87 @@
AddCSLuaFile "shared.lua"
include "shared.lua"
-- TODO:
-- https://video.google.com/get_player?wmode=opaque&ps=docs&partnerid=30&docid=0B9Kudw3An4Hnci1VZ0pwcHhJc00&enablejsapi=1
-- http://stackoverflow.com/questions/17779197/google-drive-embed-no-iframe
-- https://developers.google.com/drive/v2/reference/files/get
local APIKey = MediaPlayer.GetConfigValue('google.api_key')
local MetadataUrl = "https://www.googleapis.com/drive/v2/files/%s?key=%s"
local SupportedExtensions = { 'mp4' }
local function OnReceiveMetadata( self, callback, body )
local metadata = {}
local resp = util.JSONToTable( body )
if not resp then
return callback(false)
end
if resp.error then
return callback(false, table.Lookup(resp, 'error.message'))
end
local ext = resp.fileExtension or ''
if not table.HasValue(SupportedExtensions, ext) then
return callback(false, 'MediaPlayer currently only supports .mp4 Google Drive videos')
end
metadata.title = resp.title
metadata.thumbnail = resp.thumbnailLink
-- TODO: duration? etc.
-- no duration metadata returned :(
metadata.duration = 3600 * 4 -- default to 4 hours
self:SetMetadata(metadata, true)
MediaPlayer.Metadata:Save(self)
callback(self._metadata)
end
function SERVICE:GetMetadata( callback )
if self._metadata then
callback( self._metadata )
return
end
local cache = MediaPlayer.Metadata:Query(self)
if MediaPlayer.DEBUG then
print("MediaPlayer.GetMetadata Cache results:")
PrintTable(cache or {})
end
if cache then
local metadata = {}
metadata.title = cache.title
metadata.duration = tonumber(cache.duration)
metadata.thumbnail = cache.thumbnail
self:SetMetadata(metadata)
MediaPlayer.Metadata:Save(self)
callback(self._metadata)
else
local fileId = self:GetGoogleDriveFileId()
local apiurl = MetadataUrl:format( fileId, APIKey )
self:Fetch( apiurl,
function( body, length, headers, code )
OnReceiveMetadata( self, callback, body )
end,
function( code )
callback(false, "Failed to load YouTube ["..tostring(code).."]")
end
)
end
end

View File

@ -0,0 +1,58 @@
DEFINE_BASECLASS( "mp_service_base" )
SERVICE.Name = "Google Drive"
SERVICE.Id = "gd"
SERVICE.Base = "yt"
local GdFileIdPattern = "[%a%d-_]+"
local UrlSchemes = {
"docs%.google%.com/file/d/" .. GdFileIdPattern .. "/",
"drive%.google%.com/file/d/" .. GdFileIdPattern .. "/"
}
function SERVICE:New( url )
local obj = BaseClass.New(self, url)
obj._data = obj:GetGoogleDriveFileId()
return obj
end
function SERVICE:Match( url )
for _, pattern in pairs(UrlSchemes) do
if string.find( url, pattern ) then
return true
end
end
return false
end
function SERVICE:IsTimed()
return true
end
function SERVICE:GetGoogleDriveFileId()
local videoId
if self.videoId then
videoId = self.videoId
elseif self.urlinfo then
local url = self.urlinfo
-- https://docs.google.com/file/d/(videoId)
if url.path and string.match(url.path, "^/file/d/([%a%d-_]+)") then
videoId = string.match(url.path, "^/file/d/([%a%d-_]+)")
end
self.videoId = videoId
end
return videoId
end
-- Used for clientside inheritence of the YouTube service
SERVICE.GetYouTubeVideoId = GetGoogleDriveFileId

View File

@ -0,0 +1,57 @@
SERVICE.Name = "HTML5 Video"
SERVICE.Id = "h5v"
SERVICE.Base = "res"
SERVICE.FileExtensions = {
'webm',
-- 'mp4', -- not yet supported by Awesomium
-- 'ogg' -- already registered as audio, need a work-around :(
}
DEFINE_BASECLASS( "mp_service_base" )
if CLIENT then
local MimeTypes = {
webm = "video/webm",
mp4 = "video/mp4",
ogg = "video/ogg"
}
local EmbedHTML = [[
<video id="player" autoplay loop style="
width: 100%%;
height: 100%%;">
<source src="%s" type="%s">
</video>
]]
local JS_Volume = [[(function () {
var elem = document.getElementById('player');
if (elem) {
elem.volume = %s;
}
}());]]
function SERVICE:GetHTML()
local url = self.url
local path = self.urlinfo.path
local ext = path:match("[^/]+%.(%S+)$")
local mime = MimeTypes[ext]
return EmbedHTML:format(url, mime)
end
function SERVICE:Volume( volume )
local origVolume = volume
volume = BaseClass.Volume( self, volume )
if origVolume and ValidPanel( self.Browser ) then
self.Browser:RunJavascript(JS_Volume:format(volume))
end
end
end

View File

@ -0,0 +1,23 @@
SERVICE.Name = "Image"
SERVICE.Id = "img"
SERVICE.Base = "res"
SERVICE.FileExtensions = { 'png', 'jpg', 'jpeg', 'gif' }
if CLIENT then
local EmbedHTML = [[
<div style="background-image: url(%s);
background-repeat: no-repeat;
background-size: contain;
background-position: center center;
width: 100%%;
height: 100%%;">
</div>
]]
function SERVICE:GetHTML()
return EmbedHTML:format( self.url )
end
end

View File

@ -0,0 +1,37 @@
SERVICE.Name = "JWPlayer"
SERVICE.Id = "jw"
SERVICE.Base = "res"
SERVICE.FileExtensions = {
'mp4'
}
if CLIENT then
-- JWVideo
-- https://github.com/nexbr/playx/blob/master/lua/playx/client/handlers/default.lua
-- playxlib.GenerateJWPlayer
-- https://github.com/nexbr/playx/blob/master/lua/playxlib.lua
-- jwplayer
-- https://github.com/nexbr/playx/tree/gh-pages/js
local EmbedHTML = [[
<p>Not yet implemented</p>
]]
function SERVICE:GetHTML()
local url = self.url
local path = self.urlinfo.path
local ext = path:match("[^/]+%.(%S+)$")
local mime = MimeTypes[ext]
return EmbedHTML:format(url, mime)
end
-- TODO: Sync/volume
end

View File

@ -0,0 +1,16 @@
include "shared.lua"
DEFINE_BASECLASS( "mp_service_browser" )
function SERVICE:OnBrowserReady( browser )
BaseClass.OnBrowserReady( self, browser )
local html = self:GetHTML()
html = self.WrapHTML( html )
self.Browser:SetHTML( html )
end
function SERVICE:GetHTML()
return "<h1>SERVICE.GetHTML not yet implemented</h1>"
end

View File

@ -0,0 +1,35 @@
AddCSLuaFile "shared.lua"
include "shared.lua"
local urllib = url
local FilenamePattern = "([^/]+)%.%S+$"
local FilenameExtPattern = "([^/]+%.%S+)$"
SERVICE.TitleIncludeExtension = true -- include extension in title
function SERVICE:GetMetadata( callback )
if not self._metadata then
local title
local pattern = self.TitleIncludeExtension and
FilenameExtPattern or FilenamePattern
local path = self.urlinfo.path
path = string.match( path, pattern ) -- get filename
title = urllib.unescape( path )
self._metadata = {
title = title or self.Name,
url = self.url
}
end
if callback then
callback(self._metadata)
end
end

View File

@ -0,0 +1,21 @@
SERVICE.Name = "Resource"
SERVICE.Id = "res"
SERVICE.Base = "browser"
SERVICE.Abstract = true
SERVICE.FileExtensions = {}
function SERVICE:Match( url )
-- check supported file extensions
for _, ext in pairs(self.FileExtensions) do
if url:find("([^/]+%." .. ext .. ")$") then
return true
end
end
return false
end
function SERVICE:IsTimed()
return false
end

View File

@ -0,0 +1,15 @@
SERVICE.Name = "SHOUTcast"
SERVICE.Id = "shc"
SERVICE.Base = "af"
-- DEFINE_BASECLASS( "mp_service_af" )
local StationUrlPattern = "yp.shoutcast.com/sbin/tunein%-station%.pls%?id=%d+"
function SERVICE:Match( url )
return url:match( StationUrlPattern )
end
function SERVICE:IsTimed()
return false
end

View File

@ -0,0 +1 @@
include "shared.lua"

View File

@ -0,0 +1,104 @@
AddCSLuaFile "shared.lua"
include "shared.lua"
local urllib = url
local ClientId = MediaPlayer.GetConfigValue('soundcloud.client_id')
-- http://developers.soundcloud.com/docs/api/reference
local MetadataUrl = {
resolve = "http://api.soundcloud.com/resolve.json?url=%s&client_id=" .. ClientId,
tracks = ""
}
local function OnReceiveMetadata( self, callback, body )
local resp = util.JSONToTable(body)
if not resp then
callback(false)
return
end
if resp.errors then
callback(false, "The requested SoundCloud song wasn't found")
return
end
local artist = resp.user and resp.user.username or "[Unknown artist]"
local stream = resp.stream_url
if not stream then
callback(false, "The requested SoundCloud song doesn't allow streaming")
return
end
-- http://developers.soundcloud.com/docs/api/reference#tracks
local metadata = {}
metadata.title = (resp.title or "[Unknown title]") .. " - " .. artist
metadata.duration = math.ceil(tonumber(resp.duration) / 1000) -- responds in ms
metadata.thumbnail = resp.artwork_url
metadata.extra = {
stream = stream
}
self:SetMetadata(metadata, true)
MediaPlayer.Metadata:Save(self)
self.url = stream .. "?client_id=" .. ClientId
callback(self._metadata)
end
function SERVICE:GetMetadata( callback )
if self._metadata then
callback( self._metadata )
return
end
local cache = MediaPlayer.Metadata:Query(self)
if MediaPlayer.DEBUG then
print("MediaPlayer.GetMetadata Cache results:")
PrintTable(cache or {})
end
if cache then
local metadata = {}
metadata.title = cache.title
metadata.duration = tonumber(cache.duration)
metadata.thumbnail = cache.thumbnail
metadata.extra = cache.extra
self:SetMetadata(metadata)
MediaPlayer.Metadata:Save(self)
if metadata.extra then
local extra = util.JSONToTable(metadata.extra)
if extra.stream then
self.url = tostring(extra.stream) .. "?client_id=" .. ClientId
end
end
callback(self._metadata)
else
-- TODO: predetermine if we can skip the call to /resolve; check for
-- /track or /playlist in the url path.
local apiurl = MetadataUrl.resolve:format( self.url )
self:Fetch( apiurl,
function( body, length, headers, code )
OnReceiveMetadata( self, callback, body )
end,
function( code )
callback(false, "Failed to load YouTube ["..tostring(code).."]")
end
)
end
end

View File

@ -0,0 +1,20 @@
DEFINE_BASECLASS( "mp_service_base" )
SERVICE.Name = "SoundCloud"
SERVICE.Id = "sc"
SERVICE.Base = "af"
SERVICE.PrefetchMetadata = false
function SERVICE:New( url )
local obj = BaseClass.New(self, url)
-- TODO: grab id from /tracks/:id, etc.
obj._data = obj.urlinfo.path or '0'
return obj
end
function SERVICE:Match( url )
return string.match( url, "soundcloud.com" )
end

View File

@ -0,0 +1,56 @@
include "shared.lua"
DEFINE_BASECLASS( "mp_service_browser" )
local TwitchUrl = "http://www.twitch.tv/%s/%s/%s/popout"
---
-- Approximate amount of time it takes for the Twitch video player to load upon
-- loading the webpage.
--
local playerLoadDelay = 5
local secMinute = 60
local secHour = secMinute * 60
local function formatTwitchTime( seconds )
local hours = math.floor((seconds / secHour) % 24)
local minutes = math.floor((seconds / secMinute) % 60)
seconds = math.floor(seconds % 60)
local tbl = {}
if hours > 0 then
table.insert(tbl, hours)
table.insert(tbl, 'h')
end
if hours > 0 or minutes > 0 then
table.insert(tbl, minutes)
table.insert(tbl, 'm')
end
table.insert(tbl, seconds)
table.insert(tbl, 's')
return table.concat(tbl, '')
end
function SERVICE:OnBrowserReady( browser )
BaseClass.OnBrowserReady( self, browser )
local info = self:GetTwitchVideoInfo()
local url = TwitchUrl:format(info.channel, info.type, info.chapterId)
-- Move current time forward due to twitch player load time
local curTime = math.min( self:CurrentTime() + playerLoadDelay, self:Duration() )
local time = math.ceil( curTime )
if time > 5 then
url = url .. '?t=' .. formatTwitchTime(time)
end
browser:OpenURL( url )
end

View File

@ -0,0 +1,92 @@
AddCSLuaFile "shared.lua"
include "shared.lua"
local urllib = url
local MetadataUrl = "https://api.twitch.tv/kraken/videos/%s"
local function OnReceiveMetadata( self, callback, body )
local metadata = {}
local response = util.JSONToTable( body )
if not response then
callback(false)
return
end
-- Stream invalid
if response.status and response.status == 404 then
return callback( false, "Twitch.TV: " .. tostring(response.message) )
end
metadata.title = response.title
metadata.duration = response.length
-- Add 30 seconds to accomodate for ads in video over 5 minutes
local duration = tonumber(metadata.duration)
if duration and duration > ( 60 * 5 ) then
metadata.duration = duration + 30
end
metadata.thumbnail = response.preview
self:SetMetadata(metadata, true)
MediaPlayer.Metadata:Save(self)
callback(self._metadata)
end
function SERVICE:GetMetadata( callback )
if self._metadata then
callback( self._metadata )
return
end
local cache = MediaPlayer.Metadata:Query(self)
if MediaPlayer.DEBUG then
print("MediaPlayer.GetMetadata Cache results:")
PrintTable(cache or {})
end
if cache then
local metadata = {}
metadata.title = cache.title
metadata.duration = cache.duration
metadata.thumbnail = cache.thumbnail
self:SetMetadata(metadata)
MediaPlayer.Metadata:Save(self)
callback(self._metadata)
else
local info = self:GetTwitchVideoInfo()
-- API call fix
if info.type == 'b' then
info.type = 'a'
end
local apiurl = MetadataUrl:format( info.type .. info.chapterId )
self:Fetch( apiurl,
function( body, length, headers, code )
OnReceiveMetadata( self, callback, body )
end,
function( code )
callback(false, "Failed to load Twitch.TV ["..tostring(code).."]")
end,
-- Twitch.TV API v3 headers
{
["Accept"] = "application/vnd.twitchtv.v3+json"
}
)
end
end

View File

@ -0,0 +1,54 @@
DEFINE_BASECLASS( "mp_service_base" )
SERVICE.Name = "Twitch.TV - Video"
SERVICE.Id = "twv"
SERVICE.Base = "browser"
function SERVICE:New( url )
local obj = BaseClass.New(self, url)
local info = obj:GetTwitchVideoInfo()
obj._data = info.channel .. "_" .. info.chapterId
return obj
end
function SERVICE:Match( url )
-- TODO: should the parsed url be passed instead?
return (string.match(url, "justin.tv") or
string.match(url, "twitch.tv")) and
string.match(url, ".tv/[%w_]+/%a/%d+")
end
function SERVICE:GetTwitchVideoInfo()
local info
if self._twitchInfo then
info = self._twitchInfo
elseif self.urlinfo then
local url = self.urlinfo
local channel, type, chapterId = string.match(url.path, "^/([%w_]+)/(%a)/(%d+)")
-- Chapter videos use /c/ while archived videos use /b/
if type ~= "c" then
type = "b"
end
info = {
channel = channel,
type = type,
chapterId = chapterId
}
self._twitchInfo = info
end
return info
end

View File

@ -0,0 +1,23 @@
include "shared.lua"
DEFINE_BASECLASS( "mp_service_browser" )
local TwitchUrl = "http://www.twitch.tv/%s/popout"
local JS_HideControls = [[
document.body.style.cssText = 'overflow:hidden;height:106.8% !important';]]
function SERVICE:OnBrowserReady( browser )
BaseClass.OnBrowserReady( self, browser )
local channel = self:GetTwitchChannel()
local url = TwitchUrl:format(channel)
browser:OpenURL( url )
browser.OnFinishLoading = function(self)
self:QueueJavascript(JS_HideControls)
end
end

View File

@ -0,0 +1,61 @@
AddCSLuaFile "shared.lua"
include "shared.lua"
local urllib = url
local MetadataUrl = "https://api.twitch.tv/kraken/streams/%s"
local function OnReceiveMetadata( self, callback, body )
local metadata = {}
local response = util.JSONToTable( body )
if not response then
callback(false)
return
end
local stream = response.stream
-- Stream offline
if not stream then
return callback( false, "Twitch.TV: The requested stream was offline" )
end
local channel = stream.channel
local status = channel and channel.status or "Twitch.TV Stream"
metadata.title = status
metadata.thumbnail = stream.preview.medium
self:SetMetadata(metadata, true)
callback(self._metadata)
end
function SERVICE:GetMetadata( callback )
if self._metadata then
callback( self._metadata )
return
end
local channel = self:GetTwitchChannel()
local apiurl = MetadataUrl:format( channel )
self:Fetch( apiurl,
function( body, length, headers, code )
OnReceiveMetadata( self, callback, body )
end,
function( code )
callback(false, "Failed to load Twitch.TV ["..tostring(code).."]")
end,
-- Twitch.TV API v3 headers
{
["Accept"] = "application/vnd.twitchtv.v3+json"
}
)
end

View File

@ -0,0 +1,44 @@
DEFINE_BASECLASS( "mp_service_browser" )
SERVICE.Name = "Twitch.TV - Stream"
SERVICE.Id = "twl"
SERVICE.Base = "browser"
function SERVICE:New( url )
local obj = BaseClass.New(self, url)
local channel = obj:GetTwitchChannel()
obj._data = channel
return obj
end
function SERVICE:Match( url )
return string.match(url, "twitch.tv") and
string.match(url, ".tv/[%w_]+$")
end
function SERVICE:IsTimed()
return false
end
function SERVICE:GetTwitchChannel()
local channel
if self._twitchChannel then
channel = self._twitchChannel
elseif self.urlinfo then
local url = self.urlinfo
channel = string.match(url.path, "^/([%w_]+)")
self._twitchChannel = channel
end
return channel
end

View File

@ -0,0 +1,49 @@
include "shared.lua"
DEFINE_BASECLASS( "mp_service_browser" )
local JS_SetVolume = "if(window.MediaPlayer) MediaPlayer.setVolume(%s);"
local JS_Seek = "if(window.MediaPlayer) MediaPlayer.seek(%s);"
local function VimeoSetVolume( self )
if not self.Browser then return end
local js = JS_SetVolume:format( MediaPlayer.Volume() )
self.Browser:RunJavascript(js)
end
local function VimeoSeek( self, seekTime )
if not self.Browser then return end
local js = JS_Seek:format( seekTime )
self.Browser:RunJavascript(js)
end
function SERVICE:SetVolume( volume )
VimeoSetVolume( self )
end
function SERVICE:OnBrowserReady( browser )
BaseClass.OnBrowserReady( self, browser )
local videoId = self:GetVimeoVideoId()
-- local url = VimeoVideoUrl:format( videoId )
-- browser:OpenURL( url )
-- browser:QueueJavascript( JS_Init )
-- local html = EmbedHTML:format( videoId )
-- html = self.WrapHTML( html )
-- browser:SetHTML( html )
local url = "http://localhost/vimeo.html#" .. videoId
browser:OpenURL( url )
end
function SERVICE:Sync()
local seekTime = self:CurrentTime()
if seekTime > 0 then
VimeoSeek( self, seekTime )
end
end

View File

@ -0,0 +1,68 @@
AddCSLuaFile "shared.lua"
include "shared.lua"
local MetadataUrl = "http://vimeo.com/api/v2/video/%s.json"
local function OnReceiveMetadata( self, callback, body )
local metadata = {}
local data = util.JSONToTable( body )
if not data then
return callback( false, "Failed to parse video's metadata response." )
end
data = data[1]
metadata.title = data.title
metadata.duration = data.duration
metadata.thumbnail = data.thumbnail_medium
self:SetMetadata(metadata, true)
MediaPlayer.Metadata:Save(self)
callback(self._metadata)
end
function SERVICE:GetMetadata( callback )
if self._metadata then
callback( self._metadata )
return
end
local cache = MediaPlayer.Metadata:Query(self)
if MediaPlayer.DEBUG then
print("MediaPlayer.GetMetadata Cache results:")
PrintTable(cache or {})
end
if cache then
local metadata = {}
metadata.title = cache.title
metadata.duration = cache.duration
metadata.thumbnail = cache.thumbnail
self:SetMetadata(metadata)
MediaPlayer.Metadata:Save(self)
callback(self._metadata)
else
local videoId = self:GetVimeoVideoId()
local apiurl = MetadataUrl:format( videoId )
self:Fetch( apiurl,
function( body, length, headers, code )
OnReceiveMetadata( self, callback, body )
end,
function( code )
callback(false, "Failed to load Vimeo ["..tostring(code).."]")
end
)
end
end

View File

@ -0,0 +1,38 @@
DEFINE_BASECLASS( "mp_service_browser" )
SERVICE.Name = "Vimeo"
SERVICE.Id = "vm"
SERVICE.Base = "browser"
function SERVICE:New( url )
local obj = BaseClass.New(self, url)
obj._data = obj:GetVimeoVideoId()
return obj
end
function SERVICE:Match( url )
return string.find( url, "vimeo.com/%d+" )
end
function SERVICE:GetVimeoVideoId()
local videoId
if self.videoId then
videoId = self.videoId
elseif self.urlinfo then
local url = self.urlinfo
-- http://www.vimeo.com/(videoId)
videoId = string.match(url.path, "^/(%d+)")
self.videoId = videoId
end
return videoId
end

View File

@ -0,0 +1,179 @@
include "shared.lua"
local urllib = url
DEFINE_BASECLASS( "mp_service_browser" )
-- https://developers.google.com/youtube/player_parameters
-- TODO: add closed caption option according to cvar
SERVICE.VideoUrlFormat = "https://www.youtube.com/embed/%s?enablejsapi=1&version=3&playerapiid=ytplayer&autoplay=1&controls=0&modestbranding=1&rel=0&showinfo=0"
-- YouTube player API
-- https://developers.google.com/youtube/js_api_reference
local JS_Init = [[
if (window.MediaPlayer === undefined) {
window.MediaPlayer = (function(){
var playerId = '%s',
timed = %s;
return {
init: function () {
if (this.player) return;
this.player = document.getElementById(playerId);
if (!this.player) {
console.error('Unable to find YouTube player element!');
return false;
}
},
isPlayerReady: function () {
return ( this.ytReady ||
(this.player && (this.player.setVolume !== undefined)) );
},
setVolume: function ( volume ) {
if (!this.isPlayerReady()) return;
this.player.setVolume(volume);
},
play: function () {
if (!this.isPlayerReady()) return;
this.player.playVideo();
},
pause: function () {
if (!this.isPlayerReady()) return;
this.player.pauseVideo();
},
seek: function ( seekTime ) {
if (!this.isPlayerReady()) return;
if (!timed) return;
var state, curTime, duration, diffTime;
state = this.player.getPlayerState();
/*if (state < 0) {
this.player.playVideo();
}*/
if (state === 3) return;
duration = this.player.getDuration();
if (seekTime > duration) return;
curTime = this.player.getCurrentTime();
diffTime = Math.abs(curTime - seekTime);
if (diffTime < 5) return;
this.player.seekTo(seekTime, true);
}
};
})();
MediaPlayer.init();
}
window.onYouTubePlayerReady = function (playerId) {
MediaPlayer.ytReady = true;
MediaPlayer.init();
};
]]
local JS_SetVolume = "if(window.MediaPlayer) MediaPlayer.setVolume(%s);"
local JS_Seek = "if(window.MediaPlayer) MediaPlayer.seek(%s);"
local JS_Play = "if(window.MediaPlayer) MediaPlayer.play();"
local JS_Pause = "if(window.MediaPlayer) MediaPlayer.pause();"
local function YTSetVolume( self )
-- if not self.playerId then return end
local js = JS_SetVolume:format( MediaPlayer.Volume() * 100 )
if self.Browser then
self.Browser:RunJavascript(js)
end
end
local function YTSeek( self, seekTime )
-- if not self.playerId then return end
local js = JS_Seek:format( seekTime )
if self.Browser then
self.Browser:RunJavascript(js)
end
end
function SERVICE:SetVolume( volume )
local js = JS_SetVolume:format( MediaPlayer.Volume() * 100 )
self.Browser:RunJavascript(js)
end
function SERVICE:OnBrowserReady( browser )
BaseClass.OnBrowserReady( self, browser )
-- Resume paused player
if self._YTPaused then
self.Browser:RunJavascript( JS_Play )
self._YTPaused = nil
return
end
if not self._setupBrowser then
-- This doesn't always get called in time, but it's a nice fallback
browser:AddFunction( "window", "onYouTubePlayerReady", function( playerId )
if not playerId then return end
self.playerId = string.JavascriptSafe( playerId )
-- Initialize JavaScript MediaPlayer interface
local jsinit = JS_Init:format( self.playerId, self:IsTimed() )
browser:RunJavascript( jsinit )
YTSetVolume( self )
end )
self._setupBrowser = true
end
local videoId = self:GetYouTubeVideoId()
local url = self.VideoUrlFormat:format( videoId )
local curTime = self:CurrentTime()
-- Add start time to URL if the video didn't just begin
if self:IsTimed() and curTime > 3 then
url = url .. "&start=" .. math.Round(curTime)
end
-- Trick the embed page into thinking the referrer is youtube.com; allows
-- playing some restricted content due to the block by default behavior
-- described here: http://stackoverflow.com/a/13463245/1490006
url = urllib.escape(url)
url = "http://www.gmtower.org/apps/mediaplayer/redirect.html#" .. url
browser:OpenURL(url)
-- Initialize JavaScript MediaPlayer interface
local playerId = "player1"
local jsinit = JS_Init:format( playerId, self:IsTimed() )
browser:QueueJavascript( jsinit )
end
function SERVICE:Pause()
BaseClass.Pause( self )
if ValidPanel(self.Browser) then
self.Browser:RunJavascript(JS_Pause)
self._YTPaused = true
end
end
function SERVICE:Sync()
local seekTime = self:CurrentTime()
if seekTime > 0 then
YTSeek( self, seekTime )
end
end

View File

@ -0,0 +1,149 @@
AddCSLuaFile "shared.lua"
include "shared.lua"
-- https://developers.google.com/youtube/v3/
local APIKey = MediaPlayer.GetConfigValue('google.api_key')
local MetadataUrl = "https://www.googleapis.com/youtube/v3/videos?id=%s&key=%s&type=video&part=contentDetails,snippet,status&videoEmbeddable=true&videoSyndicated=true"
---
-- Helper function for converting ISO 8601 time strings; this is the formatting
-- used for duration specified in the YouTube v3 API.
--
-- http://stackoverflow.com/a/22149575/1490006
--
local function convertISO8601Time( duration )
local a = {}
for part in string.gmatch(duration, "%d+") do
table.insert(a, part)
end
if duration:find('M') and not (duration:find('H') or duration:find('S')) then
a = {0, a[1], 0}
end
if duration:find('H') and not duration:find('M') then
a = {a[1], 0, a[2]}
end
if duration:find('H') and not (duration:find('M') or duration:find('S')) then
a = {a[1], 0, 0}
end
duration = 0
if #a == 3 then
duration = duration + tonumber(a[1]) * 3600
duration = duration + tonumber(a[2]) * 60
duration = duration + tonumber(a[3])
end
if #a == 2 then
duration = duration + tonumber(a[1]) * 60
duration = duration + tonumber(a[2])
end
if #a == 1 then
duration = duration + tonumber(a[1])
end
return duration
end
local function OnReceiveMetadata( self, callback, body )
local metadata = {}
-- Check for valid JSON response
local resp = util.JSONToTable( body )
if not resp then
return callback(false)
end
-- If 'error' key is present, the query failed.
if resp.error then
return callback(false, table.Lookup(resp, 'error.message'))
end
-- We need at least one result
local results = table.Lookup(resp, 'pageInfo.totalResults')
if not ( results and results > 0 ) then
return callback(false, "Requested video wasn't found")
end
local item = resp.items[1]
-- Video must be embeddable
if not table.Lookup(item, 'status.embeddable') then
return callback( false, "Requested video was embed disabled" )
end
metadata.title = table.Lookup(item, 'snippet.title')
-- Check for live broadcast
local liveBroadcast = table.Lookup(item, 'snippet.liveBroadcastContent')
if liveBroadcast == 'none' then
-- Duration is an ISO 8601 string
local durationStr = table.Lookup(item, 'contentDetails.duration')
metadata.duration = math.max(1, convertISO8601Time(durationStr))
else
metadata.duration = 0 -- mark as live video
end
-- 'medium' size thumbnail doesn't have letterboxing
metadata.thumbnail = table.Lookup(item, 'snippet.thumbnails.medium.url')
self:SetMetadata(metadata, true)
if self:IsTimed() then
MediaPlayer.Metadata:Save(self)
end
callback(self._metadata)
end
function SERVICE:GetMetadata( callback )
if self._metadata then
callback( self._metadata )
return
end
local cache = MediaPlayer.Metadata:Query(self)
if MediaPlayer.DEBUG then
print("MediaPlayer.GetMetadata Cache results:")
PrintTable(cache or {})
end
if cache then
local metadata = {}
metadata.title = cache.title
metadata.duration = tonumber(cache.duration)
metadata.thumbnail = cache.thumbnail
self:SetMetadata(metadata)
if self:IsTimed() then
MediaPlayer.Metadata:Save(self)
end
callback(self._metadata)
else
local videoId = self:GetYouTubeVideoId()
local apiurl = MetadataUrl:format( videoId, APIKey )
self:Fetch( apiurl,
function( body, length, headers, code )
OnReceiveMetadata( self, callback, body )
end,
function( code )
callback(false, "Failed to load YouTube ["..tostring(code).."]")
end
)
end
end

View File

@ -0,0 +1,79 @@
DEFINE_BASECLASS( "mp_service_base" )
SERVICE.Name = "YouTube"
SERVICE.Id = "yt"
SERVICE.Base = "browser"
local YtVideoIdPattern = "[%a%d-_]+"
local UrlSchemes = {
"youtube%.com/watch%?v=" .. YtVideoIdPattern,
"youtu%.be/watch%?v=" .. YtVideoIdPattern,
"youtube%.com/v/" .. YtVideoIdPattern,
"youtu%.be/v/" .. YtVideoIdPattern,
"youtube%.googleapis%.com/v/" .. YtVideoIdPattern
}
function SERVICE:New( url )
local obj = BaseClass.New(self, url)
obj._data = obj:GetYouTubeVideoId()
return obj
end
function SERVICE:Match( url )
for _, pattern in pairs(UrlSchemes) do
if string.find( url, pattern ) then
return true
end
end
return false
end
function SERVICE:IsTimed()
if self._istimed == nil then
-- YouTube Live resolves to 0 second video duration
self._istimed = self:Duration() > 0
end
return self._istimed
end
function SERVICE:GetYouTubeVideoId()
local videoId
if self.videoId then
videoId = self.videoId
elseif self.urlinfo then
local url = self.urlinfo
-- http://www.youtube.com/watch?v=(videoId)
if url.query and url.query.v then
videoId = url.query.v
-- http://www.youtube.com/v/(videoId)
elseif url.path and string.match(url.path, "^/v/([%a%d-_]+)") then
videoId = string.match(url.path, "^/v/([%a%d-_]+)")
-- http://youtube.googleapis.com/v/(videoId)
elseif url.path and string.match(url.path, "^/v/([%a%d-_]+)") then
videoId = string.match(url.path, "^/v/([%a%d-_]+)")
-- http://youtu.be/(videoId)
elseif string.match(url.host, "youtu.be") and
url.path and string.match(url.path, "^/([%a%d-_]+)$") and
( (not url.query) or #url.query == 0 ) then -- short url
videoId = string.match(url.path, "^/([%a%d-_]+)$")
end
self.videoId = videoId
end
return videoId
end

View File

@ -0,0 +1,105 @@
--[[---------------------------------------------------------
Media Player History
-----------------------------------------------------------]]
MediaPlayer.History = {}
---
-- Default metadata table name
-- @type String
--
local TableName = "mediaplayer_history"
---
-- SQLite table struct
-- @type String
--
local TableStruct = string.format([[
CREATE TABLE %s (
id INTEGER PRIMARY KEY AUTOINCREMENT,
mediaid VARCHAR(48),
url VARCHAR(512),
player_name VARCHAR(32),
steamid VARCHAR(32),
time DATETIME DEFAULT CURRENT_TIMESTAMP
)]], TableName)
---
-- Default number of results to return
-- @type Integer
--
local DefaultResultLimit = 100
---
-- Log the given media as a request.
--
-- @param media Media service object.
-- @return table SQL query results.
--
function MediaPlayer.History:LogRequest( media )
local id = media:UniqueID()
if not id then return end
local ply = media:GetOwner()
if not IsValid(ply) then return end
local query = string.format( "INSERT INTO `%s` " ..
"(mediaid,url,player_name,steamid) " ..
"VALUES ('%s',%s,%s,'%s')",
TableName,
media:UniqueID(),
sql.SQLStr( media:Url() ),
sql.SQLStr( ply:Nick() ),
ply:SteamID64() )
local result = sql.Query(query)
if MediaPlayer.DEBUG then
print("MediaPlayer.History.LogRequest")
print(query)
if istable(result) then
PrintTable(result)
else
print(result)
end
end
return result
end
function MediaPlayer.History:GetRequestsByPlayer( ply, limit )
if not isnumber(limit) then
limit = DefaultResultLimit
end
local query = string.format( [[
SELECT H.*, M.title, M.thumbnail, M.duration
FROM %s AS H
JOIN mediaplayer_metadata AS M
ON (M.id = H.mediaid)
WHERE steamid='%s'
LIMIT %d]],
TableName,
ply:SteamID64(),
limit )
local result = sql.Query(query)
if MediaPlayer.DEBUG then
print("MediaPlayer.History.GetRequestsByPlayer", ply, limit)
print(query)
if istable(result) then
PrintTable(result)
else
print(result)
end
end
return result
end
-- Create the SQLite table if it doesn't exist
if not sql.TableExists(TableName) then
Msg("MediaPlayer.History: Creating `" .. TableName .. "` table...\n")
print(sql.Query(TableStruct))
end

View File

@ -0,0 +1,198 @@
--[[---------------------------------------------------------
Media Player Types
-----------------------------------------------------------]]
MediaPlayer.Type = {}
---
-- Registers a media player type.
--
-- @param tbl Media player type table.
--
function MediaPlayer.Register( tbl )
local name = tbl.Name
if not name then
ErrorNoHalt("MediaPlayer.Register - Must include name property\n")
debug.Trace()
return
end
name = name:lower() -- always use lowercase names
tbl.Name = name
tbl.__index = tbl
-- Set base meta table
local base = tbl.Base or "base"
if base and name ~= "base" then
base = base:lower()
if not MediaPlayer.Type[base] then
ErrorNoHalt("MediaPlayer.Register - Invalid base name: " .. base .. "\n")
debug.Trace()
return
end
base = MediaPlayer.Type[base]
setmetatable(tbl, {
__index = base,
__tostring = base.__tostring
})
end
local classname = "mp_" .. name
-- Store media player type as a base class
baseclass.Set( classname, tbl )
-- Save player type
MediaPlayer.Type[name] = tbl
if MediaPlayer.DEBUG then
Msg( "MediaPlayer.Register\t" .. name .. "\n" )
end
end
function MediaPlayer.IsValidType( type )
return MediaPlayer.Type[type] ~= nil
end
-- Load players
do
local path = "players/"
local players = {
"base", -- MUST LOAD FIRST!
"entity"
}
for _, player in ipairs(players) do
local clfile = path .. player .. "/cl_init.lua"
local svfile = path .. player .. "/init.lua"
MEDIAPLAYER = {}
if SERVER then
AddCSLuaFile(clfile)
include(svfile)
else
include(clfile)
end
MediaPlayer.Register( MEDIAPLAYER )
MEDIAPLAYER = nil
end
end
--[[---------------------------------------------------------
Media Player Helpers
-----------------------------------------------------------]]
MediaPlayer.List = MediaPlayer.List or {}
MediaPlayer._count = MediaPlayer._count or 0
---
-- Creates a media player object.
--
-- @param id Media player ID.
-- @param type? Media player type (defaults to 'base').
-- @return table Media player object.
--
function MediaPlayer.Create( id, type )
-- Inherit media player type
local PlayerType = MediaPlayer.Type[type]
PlayerType = PlayerType or MediaPlayer.Type.base
-- Create media player object
local mp = setmetatable( {}, { __index = PlayerType } )
-- Assign unique ID
if id then
mp.id = id
elseif SERVER then
MediaPlayer._count = MediaPlayer._count + 1
mp.id = MediaPlayer._count
else
mp.id = id or -1
end
mp:Init()
-- Add to media player list
MediaPlayer.List[mp.id] = mp
if MediaPlayer.DEBUG then
print( "Created Media Player", mp, mp.Name, type )
end
return mp
end
---
-- Destroys the given media player object.
--
-- @param table Media player object.
--
function MediaPlayer.Destroy( mp )
-- TODO: does this need anything else?
MediaPlayer.List[mp.id] = nil
if MediaPlayer.DEBUG then
print( "Destroyed Media Player '" .. tostring(mp.id) .. "'" )
end
end
---
-- Gets the media player associated with the given ID.
--
-- @param id Media player ID.
-- @return table Media player object.
--
function MediaPlayer.GetById( id )
local mp = MediaPlayer.List[id]
if mp then
return mp
else
-- Since entity indexes can change, let's iterate the list just to
-- be sure...
for _, mp in pairs(MediaPlayer.List) do
if mp:GetId() == id then
return mp
end
end
end
end
---
-- Gets all active media players.
--
-- @return table Array of all active media players.
--
function MediaPlayer.GetAll()
return MediaPlayer.List
end
--[[---------------------------------------------------------
Media Player Think Loop
-----------------------------------------------------------]]
MediaPlayer.ThinkInterval = 0.2 -- seconds
local function MediaPlayerThink()
for id, mp in pairs( MediaPlayer.List ) do
mp:Think()
end
end
if timer.Exists( "MediaPlayerThink" ) then
timer.Destroy( "MediaPlayerThink" )
end
-- TODO: only start timer when at least one mediaplayer is created; stop it when
-- there are none left.
timer.Create( "MediaPlayerThink", MediaPlayer.ThinkInterval, 0, MediaPlayerThink )
timer.Start( "MediaPlayerThink" )

View File

@ -0,0 +1,127 @@
MediaPlayer.Services = {}
function MediaPlayer.RegisterService( service )
local base
if service.Base then
base = MediaPlayer.Services[service.Base]
elseif MediaPlayer.Services.base then
base = MediaPlayer.Services.base
end
-- Inherit base service
setmetatable( service, { __index = base } )
-- Create base class for service
baseclass.Set( "mp_service_" .. service.Id, service )
-- Store service
MediaPlayer.Services[ service.Id ] = service
if MediaPlayer.DEBUG then
print( "MediaPlayer.RegisterService", service.Name )
end
end
function MediaPlayer.GetValidServiceNames( whitelist )
local tbl = {}
for _, service in pairs(MediaPlayer.Services) do
if not rawget(service, "Abstract") then
if whitelist then
if table.HasValue( whitelist, service.Id ) then
table.insert( tbl, service.Name )
end
else
table.insert( tbl, service.Name )
end
end
end
return tbl
end
function MediaPlayer.ValidUrl( url )
for id, service in pairs(MediaPlayer.Services) do
if service:Match( url ) then
return true
end
end
return false
end
function MediaPlayer.GetMediaForUrl( url )
local service
for id, s in pairs(MediaPlayer.Services) do
if s:Match( url ) then
service = s
break
end
end
if not service then
service = MediaPlayer.Services.base
end
return service:New( url )
end
-- Load services
do
local path = "services/"
local fullpath = "autorun/mediaplayer/" .. path
local services = {
"base", -- MUST LOAD FIRST!
-- Browser
"browser", -- base
"youtube",
"googledrive",
"twitch",
"twitchstream",
"vimeo",
-- HTML Resources
"resource", -- base
"image",
"html5_video",
-- IGModAudioChannel
"audiofile",
"shoutcast",
"soundcloud"
}
for _, name in ipairs(services) do
local clfile = path .. name .. "/cl_init.lua"
local svfile = path .. name .. "/init.lua"
local shfile = fullpath .. name .. ".lua"
if file.Exists(shfile, "LUA") then
clfile = shfile
svfile = shfile
end
SERVICE = {}
if SERVER then
AddCSLuaFile(clfile)
include(svfile)
else
include(clfile)
end
MediaPlayer.RegisterService( SERVICE )
SERVICE = nil
end
end

View File

@ -0,0 +1,78 @@
MediaPlayer = MediaPlayer or {}
--[[---------------------------------------------------------
ConVars
-----------------------------------------------------------]]
MediaPlayer.Cvars = {}
MediaPlayer.Cvars.Debug = CreateConVar( "mediaplayer_debug", 0, {FCVAR_ARCHIVE,FCVAR_DONTRECORD}, "Enables media player debug mode; logs a bunch of actions into the console." )
MediaPlayer.DEBUG = MediaPlayer.Cvars.Debug:GetBool()
cvars.AddChangeCallback( "mediaplayer_debug", function(name, old, new)
MediaPlayer.DEBUG = new == 1
end)
if SERVER then
AddCSLuaFile "cl_cvars.lua"
else
include "cl_cvars.lua"
end
--[[---------------------------------------------------------
Config
Store service API keys, etc.
-----------------------------------------------------------]]
MediaPlayer.config = {}
---
-- Apply configuration values to the mediaplayer config.
--
-- @param config Table with configuration values.
--
function MediaPlayer.SetConfig( config )
table.Merge( MediaPlayer.config, config )
end
---
-- Method for easily grabbing config value without checking that each fragment
-- exists.
--
-- @param key e.g. "json.key.fragments"
--
function MediaPlayer.GetConfigValue( key )
local value = table.Lookup( MediaPlayer.config, key )
if type(value) == 'nil' then
ErrorNoHalt("WARNING: MediaPlayer config value not found for key `" .. tostring(key) .. "`\n")
end
return value
end
if SERVER then
include "config/server.lua"
end
--[[---------------------------------------------------------
Shared includes
-----------------------------------------------------------]]
include "sh_mediaplayer.lua"
include "sh_services.lua"
include "sh_history.lua"
hook.Add("Initialize", "InitMediaPlayer", function()
hook.Run("InitMediaPlayer", MediaPlayer)
end)
-- No fun allowed
hook.Add( "CanDrive", "DisableMediaPlayerDriving", function(ply, ent)
if IsValid(ent) and ent.IsMediaPlayerEntity then
return IsValid(ply) and ply:IsAdmin()
end
end)

View File

@ -0,0 +1,174 @@
--[[---------------------------------------------------------
Media Player Metadata
All media metadata is cached in an SQLite table for quick
lookup and to prevent unnecessary network requests.
-----------------------------------------------------------]]
MediaPlayer.Metadata = {}
---
-- Default metadata table name
-- @type String
--
local TableName = "mediaplayer_metadata"
---
-- SQLite table struct
-- @type String
--
local TableStruct = string.format([[
CREATE TABLE %s (
id VARCHAR(48) PRIMARY KEY,
title VARCHAR(128),
duration INTEGER NOT NULL DEFAULT 0,
thumbnail VARCHAR(512),
extra VARCHAR(2048),
request_count INTEGER NOT NULL DEFAULT 1,
last_request INTEGER NOT NULL DEFAULT 0,
last_updated INTEGER NOT NULL DEFAULT 0,
expired BOOLEAN NOT NULL DEFAULT 0
)]], TableName)
---
-- Maximum cache age before it expires; currently one week in seconds.
-- @type Number
--
local MaxCacheAge = 604800
---
-- Query the metadata table for the given media object's metadata.
-- If the metadata is older than one week, it is ignored and replaced upon
-- saving.
--
-- @param media Media service object.
-- @return table Cached metadata results.
--
function MediaPlayer.Metadata:Query( media )
local id = media:UniqueID()
if not id then return end
local query = ("SELECT * FROM `%s` WHERE id='%s'"):format(TableName, id)
if MediaPlayer.DEBUG then
print("MediaPlayer.Metadata.Query")
print(query)
end
local results = sql.QueryRow(query)
if results then
local expired = ( tonumber(results.expired) == 1 )
-- Media metadata has been marked as out-of-date
if expired then
return nil
end
local lastupdated = tonumber( results.last_updated )
local timediff = os.time() - lastupdated
if timediff > MaxCacheAge then
-- Set metadata entry as expired
query = "UPDATE `%s` SET expired=1 WHERE id='%s'"
query = query:format( TableName, id )
if MediaPlayer.DEBUG then
print("MediaPlayer.Metadata.Query: Setting entry as expired")
print(query)
end
sql.Query( query )
return nil
else
return results
end
elseif results == false then
ErrorNoHalt("MediaPlayer.Metadata.Query: There was an error executing the SQL query\n")
print(query)
end
return nil
end
---
-- Save or update the given media object into the metadata table.
--
-- @param media Media service object.
-- @return table SQL query results.
--
function MediaPlayer.Metadata:Save( media )
local id = media:UniqueID()
if not id then return end
local query = ("SELECT expired FROM `%s` WHERE id='%s'"):format(TableName, id)
local results = sql.Query(query)
if istable(results) then -- update
if MediaPlayer.DEBUG then
print("MediaPlayer.Metadata.Save Results:")
PrintTable(results)
end
results = results[1]
local expired = ( tonumber(results.expired) == 1 )
if expired then
-- Update possible new metadata
query = "UPDATE `%s` SET request_count=request_count+1, title=%s, duration=%s, thumbnail=%s, extra=%s, last_request=%s, last_updated=%s, expired=0 WHERE id='%s'"
query = query:format( TableName,
sql.SQLStr( media:Title() ),
media:Duration(),
sql.SQLStr( media:Thumbnail() ),
sql.SQLStr( util.TableToJSON(media._metadata.extra) ),
os.time(),
os.time(),
id )
else
query = "UPDATE `%s` SET request_count=request_count+1, last_request=%s WHERE id='%s'"
query = query:format( TableName, os.time(), id )
end
else -- insert
query = string.format( "INSERT INTO `%s` ", TableName ) ..
"(id,title,duration,thumbnail,extra,last_request,last_updated) VALUES (" ..
string.format( "'%s',", id ) ..
string.format( "%s,", sql.SQLStr( media:Title() ) ) ..
string.format( "%s,", media:Duration() ) ..
string.format( "%s,", sql.SQLStr( media:Thumbnail() ) ) ..
string.format( "%s,", sql.SQLStr( util.TableToJSON(media._metadata.extra) ) ) ..
string.format( "%d,", os.time() ) ..
string.format( "%d)", os.time() )
end
if MediaPlayer.DEBUG then
print("MediaPlayer.Metadata.Save")
print(query)
end
results = sql.Query(query)
if results == false then
ErrorNoHalt("MediaPlayer.Metadata.Save: There was an error executing the SQL query\n")
print(query)
end
return results
end
-- Create the SQLite table if it doesn't exist
if not sql.TableExists(TableName) then
Msg("MediaPlayer.Metadata: Creating `" .. TableName .. "` table...\n")
sql.Query(TableStruct)
end

View File

@ -0,0 +1,8 @@
hook.Add( "PopulateMenuBar", "MediaPlayerOptions_MenuBar", function( menubar )
local m = menubar:AddOrGetMenu( "Media Player" )
m:AddCVar( "Fullscreen", "mediaplayer_fullscreen", "1", "0" )
end )

View File

@ -0,0 +1,185 @@
AddCSLuaFile()
local mporder = 3200
--
-- Adds a media player property.
--
-- Blue icons correspond to admin actions.
--
local function AddMediaPlayerProperty( name, config )
-- Assign incrementing order ID
config.Order = mporder
mporder = mporder + 1
properties.Add( name, config )
end
local function IsMediaPlayer( self, ent, ply )
return IsValid(ent) and IsValid(ply) and
IsValid(ent:GetMediaPlayer()) and
gamemode.Call( "CanProperty", ply, self.InternalName, ent )
end
local function IsPrivilegedMediaPlayer( self, ent, ply )
return IsMediaPlayer( self, ent, ply ) and
( ply:IsAdmin() or ent:GetOwner() == ply )
end
local function HasMedia( mp )
return mp:GetPlayerState() >= MP_STATE_PLAYING
end
AddMediaPlayerProperty( "mp-pause", {
MenuLabel = "Pause",
MenuIcon = "icon16/control_pause_blue.png",
Filter = function( self, ent, ply )
if not IsPrivilegedMediaPlayer(self, ent, ply) then return end
local mp = ent:GetMediaPlayer()
return IsValid(mp) and mp:GetPlayerState() == MP_STATE_PLAYING
end,
Action = function( self, ent )
MediaPlayer.Pause( ent )
end
})
AddMediaPlayerProperty( "mp-resume", {
MenuLabel = "Resume",
MenuIcon = "icon16/control_play_blue.png",
Filter = function( self, ent, ply )
if not IsPrivilegedMediaPlayer(self, ent, ply) then return end
local mp = ent:GetMediaPlayer()
return IsValid(mp) and mp:GetPlayerState() == MP_STATE_PAUSED
end,
Action = function( self, ent )
MediaPlayer.Pause( ent )
end
})
AddMediaPlayerProperty( "mp-skip", {
MenuLabel = "Skip",
MenuIcon = "icon16/control_end_blue.png",
Filter = function( self, ent, ply )
if not IsPrivilegedMediaPlayer(self, ent, ply) then return end
local mp = ent:GetMediaPlayer()
return IsValid(mp) and HasMedia(mp)
end,
Action = function( self, ent )
MediaPlayer.Skip( ent )
end
})
AddMediaPlayerProperty( "mp-seek", {
MenuLabel = "Seek",
-- MenuIcon = "icon16/timeline_marker.png",
MenuIcon = "icon16/control_fastforward_blue.png",
Filter = function( self, ent, ply )
if not IsPrivilegedMediaPlayer(self, ent, ply) then return end
local mp = ent:GetMediaPlayer()
return IsValid(mp) and HasMedia(mp)
end,
Action = function( self, ent )
Derma_StringRequest(
"Media Player",
"Enter a time in HH:MM:SS format (hours, minutes, seconds):",
"", -- Default text
function( time )
MediaPlayer.Seek( ent, time )
end,
function() end,
"Seek",
"Cancel"
)
end
})
AddMediaPlayerProperty( "mp-request-url", {
MenuLabel = "Request URL",
MenuIcon = "icon16/link_add.png",
Filter = IsMediaPlayer,
Action = function( self, ent )
--[[ Old request dialog
Derma_StringRequest(
"Media Player", -- Title
"Enter a URL to request:", -- Subtitle
"", -- Default text
function( url )
MediaPlayer.Request( ent, url )
end,
function() end,
"Request",
"Cancel"
)
]]
MediaPlayer.OpenRequestMenu( ent )
end
})
AddMediaPlayerProperty( "mp-copy-url", {
MenuLabel = "Copy URL to clipboard",
-- MenuIcon = "icon16/link.png",
MenuIcon = "icon16/paste_plain.png",
Filter = function( self, ent, ply )
if not IsMediaPlayer(self, ent, ply) then return end
local mp = ent:GetMediaPlayer()
return IsValid(mp) and HasMedia(mp)
end,
Action = function( self, ent )
local mp = ent:GetMediaPlayer()
local media = mp and mp:CurrentMedia()
if not IsValid(media) then return end
SetClipboardText( media:Url() )
LocalPlayer():ChatPrint( "Media URL has been copied into your clipboard." )
end
})
AddMediaPlayerProperty( "mp-enable", {
MenuLabel = "Turn On",
MenuIcon = "icon16/lightbulb.png",
Filter = function( self, ent, ply )
return IsValid(ent) and IsValid(ply) and
ent.IsMediaPlayerEntity and
not IsValid(ent:GetMediaPlayer()) and
gamemode.Call( "CanProperty", ply, self.InternalName, ent )
end,
Action = function( self, ent )
MediaPlayer.RequestListen( ent )
end
})
AddMediaPlayerProperty( "mp-disable", {
MenuLabel = "Turn Off",
MenuIcon = "icon16/lightbulb_off.png",
Filter = function( self, ent, ply )
return IsValid(ent) and IsValid(ply) and
ent.IsMediaPlayerEntity and
IsValid(ent:GetMediaPlayer()) and
gamemode.Call( "CanProperty", ply, self.InternalName, ent )
end,
Action = function( self, ent )
MediaPlayer.RequestListen( ent )
end
})

View File

@ -0,0 +1,84 @@
AddCSLuaFile()
ENT.Type = "anim"
ENT.Base = "base_anim"
ENT.Spawnable = false
ENT.Model = Model( "models/gmod_tower/suitetv.mdl" )
ENT.MediaPlayerType = "entity"
ENT.UseDelay = 0.5 -- seconds
ENT.IsMediaPlayerEntity = true
function ENT:Initialize()
if SERVER then
self:SetModel( self.Model )
self:SetUseType( SIMPLE_USE )
self:PhysicsInit( SOLID_VPHYSICS )
self:SetMoveType( MOVETYPE_VPHYSICS )
local phys = self:GetPhysicsObject()
if IsValid( phys ) then
phys:EnableMotion( false )
end
self:DrawShadow( false )
-- Install media player to entity
self:InstallMediaPlayer( self.MediaPlayerType )
-- Network media player ID
local mp = self:GetMediaPlayer()
self:SetMediaPlayerID( mp:GetId() )
end
end
function ENT:SetupDataTables()
self:NetworkVar( "String", 0, "MediaPlayerID" )
end
function ENT:Use(ply)
if not IsValid(ply) then return end
-- Delay request
if ply.NextUse and ply.NextUse > CurTime() then
return
end
local mp = self:GetMediaPlayer()
if not mp then
ErrorNoHalt("MediaPlayer test entity doesn't have player installed\n")
debug.Trace()
return
end
if mp:HasListener(ply) then
mp:RemoveListener(ply)
else
mp:AddListener(ply)
end
ply.NextUse = CurTime() + self.UseDelay
end
function ENT:OnRemove()
local mp = self:GetMediaPlayer()
if mp then
mp:Remove()
end
end
function ENT:UpdateTransmitState()
return TRANSMIT_PVS
end

View File

@ -0,0 +1,20 @@
AddCSLuaFile()
ENT.PrintName = "Media Player Example"
ENT.Author = "Samuel Maddock"
ENT.Instructions = "Right click on the TV to see available Media Player options. Alternatively, press E on the TV to turn it on."
ENT.Category = "Media Player"
ENT.Type = "anim"
ENT.Base = "mediaplayer_base"
ENT.Spawnable = true
ENT.Model = Model( "models/gmod_tower/suitetv_large.mdl" )
ENT.PlayerConfig = {
angle = Angle(-90, 90, 0),
offset = Vector(6, 59.49, 103.65),
width = 119,
height = 69
}

View File

@ -0,0 +1,86 @@
AddCSLuaFile()
DEFINE_BASECLASS( "mediaplayer_base" )
ENT.PrintName = "Media Player Projector"
ENT.Author = "Samuel Maddock"
ENT.Instructions = "Press Use on the entity to start watching."
ENT.Category = "Media Player"
ENT.Type = "anim"
ENT.Base = "mediaplayer_base"
ENT.RenderGroup = RENDERGROUP_BOTH
ENT.Spawnable = true
ENT.Model = Model( "models/props/cs_office/projector.mdl" )
function ENT:SetupDataTables()
self:NetworkVar( "Entity", 0, "Flashlight" )
end
-- TODO: figure out how to get this to work; will probably involve using a
-- render target
if SERVER then
function ENT:Initialize()
BaseClass.Initialize(self)
self.flashlight = ents.Create( "env_projectedtexture" )
self.flashlight:SetParent( self.Entity )
-- The local positions are the offsets from parent..
self.flashlight:SetLocalPos( Vector( 0, 0, 0 ) )
self.flashlight:SetLocalAngles( Angle(0,0,0) )
-- Looks like only one flashlight can have shadows enabled!
self.flashlight:SetKeyValue( "enableshadows", 1 )
self.flashlight:SetKeyValue( "farz", 1024 )
self.flashlight:SetKeyValue( "nearz", 12 )
self.flashlight:SetKeyValue( "lightfov", 90 )
local c = self:GetColor()
local b = 1
self.flashlight:SetKeyValue( "lightcolor", Format( "%i %i %i 255", c.r * b, c.g * b, c.b * b ) )
self.flashlight:Spawn()
self.flashlight:Input( "SpotlightTexture", NULL, NULL, "vgui/hand.vtf" )
self:SetFlashlight(self.flashlight)
end
else
local projmat = CreateMaterial( "projmat", "UnlitGeneric", {
["$basetexture"] = "vgui/hand.vtf"
})
function ENT:Draw()
BaseClass.Draw(self)
local flashlight = self:GetFlashlight()
if not IsValid(flashlight) then return end
local mp = self:GetMediaPlayer()
if not IsValid(mp) then return end
local media = mp:CurrentMedia()
local browser = media and media.Browser or MediaPlayer.GetIdlescreen()
browser:UpdateHTMLTexture()
local mat = browser:GetHTMLMaterial()
local texture = mat:GetTexture("$basetexture")
projmat:SetTexture( "$basetexture", texture )
end
end

View File

@ -0,0 +1,8 @@
{
"folders":
[
{
"path": "."
}
]
}

Binary file not shown.