mirror of
https://github.com/samuelmaddock/gm-mediaplayer.git
synced 2025-03-04 03:03:02 -05:00
Initial commit
This commit is contained in:
commit
5e1d6fe6d0
18
.editorconfig
Normal file
18
.editorconfig
Normal 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
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
# general
|
||||
*.todo
|
||||
*.sublime-workspace
|
||||
|
||||
# old mediaplayer service code
|
||||
lua/autorun/mediaplayer/services/_deprecated
|
21
LICENSE
Normal file
21
LICENSE
Normal 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
12
README.md
Normal 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
10
addon.txt
Normal 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"
|
||||
}
|
15
content/materials/theater/STATIC.vmt
Normal file
15
content/materials/theater/STATIC.vmt
Normal file
@ -0,0 +1,15 @@
|
||||
"UnlitGeneric"
|
||||
{
|
||||
"$basetexture" "theater/STATIC"
|
||||
"$surfaceprop" "glass"
|
||||
"%keywords" "theater"
|
||||
"Proxies"
|
||||
{
|
||||
"AnimatedTexture"
|
||||
{
|
||||
"animatedTextureVar" "$basetexture"
|
||||
"animatedTextureFrameNumVar" "$frame"
|
||||
"animatedTextureFrameRate" "16"
|
||||
}
|
||||
}
|
||||
}
|
BIN
content/materials/theater/STATIC.vtf
Normal file
BIN
content/materials/theater/STATIC.vtf
Normal file
Binary file not shown.
1
html/.gitattributes
vendored
Normal file
1
html/.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
||||
* text=auto
|
6
html/.gitignore
vendored
Normal file
6
html/.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
node_modules
|
||||
dist
|
||||
.tmp
|
||||
.sass-cache
|
||||
bower_components
|
||||
test/bower_components
|
21
html/.jshintrc
Normal file
21
html/.jshintrc
Normal 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
406
html/Gruntfile.js
Normal 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
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
24
html/app/index.html
Normal 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
16
html/app/scripts/main.js
Normal 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
17
html/app/styles/main.scss
Normal 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
95
html/app/vimeo.html
Normal 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>
|
642
html/app/vimeo_playground.html
Normal file
642
html/app/vimeo_playground.html
Normal 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&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, '<').replace(/>/g, '>');
|
||||
|
||||
// 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
8
html/bower.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "html",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"jquery": "~1.11.0"
|
||||
},
|
||||
"devDependencies": {}
|
||||
}
|
33
html/package.json
Normal file
33
html/package.json
Normal 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
3
html/test/.bowerrc
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"directory": "bower_components"
|
||||
}
|
9
html/test/bower.json
Normal file
9
html/test/bower.json
Normal 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
27
html/test/index.html
Normal 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
13
html/test/spec/test.js
Normal 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 () {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
})();
|
76
lua/autorun/includes/extensions/cl_draw.lua
Normal file
76
lua/autorun/includes/extensions/cl_draw.lua
Normal 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
|
10
lua/autorun/includes/extensions/sh_file.lua
Normal file
10
lua/autorun/includes/extensions/sh_file.lua
Normal 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
|
9
lua/autorun/includes/extensions/sh_math.lua
Normal file
9
lua/autorun/includes/extensions/sh_math.lua
Normal 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
|
21
lua/autorun/includes/extensions/sh_table.lua
Normal file
21
lua/autorun/includes/extensions/sh_table.lua
Normal 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
|
452
lua/autorun/includes/extensions/sh_url.lua
Normal file
452
lua/autorun/includes/extensions/sh_url.lua
Normal 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 = {
|
||||
[' '] = ' ',
|
||||
['¡'] = '¡',
|
||||
['¢'] = '¢',
|
||||
['£'] = '£',
|
||||
['¤'] = '¤',
|
||||
['¥'] = '¥',
|
||||
['¦'] = '¦',
|
||||
['§'] = '§',
|
||||
['¨'] = '¨',
|
||||
['©'] = '©',
|
||||
['ª'] = 'ª',
|
||||
['«'] = '«',
|
||||
['¬'] = '¬',
|
||||
[''] = '­',
|
||||
['®'] = '®',
|
||||
['¯'] = '¯',
|
||||
['°'] = '°',
|
||||
['±'] = '±',
|
||||
['²'] = '²',
|
||||
['³'] = '³',
|
||||
['´'] = '´',
|
||||
['µ'] = 'µ',
|
||||
['¶'] = '¶',
|
||||
['·'] = '·',
|
||||
['¸'] = '¸',
|
||||
['¹'] = '¹',
|
||||
['º'] = 'º',
|
||||
['»'] = '»',
|
||||
['¼'] = '¼',
|
||||
['½'] = '½',
|
||||
['¾'] = '¾',
|
||||
['¿'] = '¿',
|
||||
['À'] = 'À',
|
||||
['Á'] = 'Á',
|
||||
['Â'] = 'Â',
|
||||
['Ã'] = 'Ã',
|
||||
['Ä'] = 'Ä',
|
||||
['Å'] = 'Å',
|
||||
['Æ'] = 'Æ',
|
||||
['Ç'] = 'Ç',
|
||||
['È'] = 'È',
|
||||
['É'] = 'É',
|
||||
['Ê'] = 'Ê',
|
||||
['Ë'] = 'Ë',
|
||||
['Ì'] = 'Ì',
|
||||
['Í'] = 'Í',
|
||||
['Î'] = 'Î',
|
||||
['Ï'] = 'Ï',
|
||||
['Ð'] = 'Ð',
|
||||
['Ñ'] = 'Ñ',
|
||||
['Ò'] = 'Ò',
|
||||
['Ó'] = 'Ó',
|
||||
['Ô'] = 'Ô',
|
||||
['Õ'] = 'Õ',
|
||||
['Ö'] = 'Ö',
|
||||
['×'] = '×',
|
||||
['Ø'] = 'Ø',
|
||||
['Ù'] = 'Ù',
|
||||
['Ú'] = 'Ú',
|
||||
['Û'] = 'Û',
|
||||
['Ü'] = 'Ü',
|
||||
['Ý'] = 'Ý',
|
||||
['Þ'] = 'Þ',
|
||||
['ß'] = 'ß',
|
||||
['à'] = 'à',
|
||||
['á'] = 'á',
|
||||
['â'] = 'â',
|
||||
['ã'] = 'ã',
|
||||
['ä'] = 'ä',
|
||||
['å'] = 'å',
|
||||
['æ'] = 'æ',
|
||||
['ç'] = 'ç',
|
||||
['è'] = 'è',
|
||||
['é'] = 'é',
|
||||
['ê'] = 'ê',
|
||||
['ë'] = 'ë',
|
||||
['ì'] = 'ì',
|
||||
['í'] = 'í',
|
||||
['î'] = 'î',
|
||||
['ï'] = 'ï',
|
||||
['ð'] = 'ð',
|
||||
['ñ'] = 'ñ',
|
||||
['ò'] = 'ò',
|
||||
['ó'] = 'ó',
|
||||
['ô'] = 'ô',
|
||||
['õ'] = 'õ',
|
||||
['ö'] = 'ö',
|
||||
['÷'] = '÷',
|
||||
['ø'] = 'ø',
|
||||
['ù'] = 'ù',
|
||||
['ú'] = 'ú',
|
||||
['û'] = 'û',
|
||||
['ü'] = 'ü',
|
||||
['ý'] = 'ý',
|
||||
['þ'] = 'þ',
|
||||
['ÿ'] = 'ÿ',
|
||||
['"'] = '"',
|
||||
["'"] = ''',
|
||||
['<'] = '<',
|
||||
['>'] = '>',
|
||||
['&'] = '&'
|
||||
}
|
||||
|
||||
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
|
179
lua/autorun/includes/modules/EventEmitter.lua
Normal file
179
lua/autorun/includes/modules/EventEmitter.lua
Normal 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
|
299
lua/autorun/includes/modules/browserpool.lua
Normal file
299
lua/autorun/includes/modules/browserpool.lua
Normal 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
|
96
lua/autorun/includes/modules/control.lua
Normal file
96
lua/autorun/includes/modules/control.lua
Normal 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
|
140
lua/autorun/includes/modules/htmlmaterial.lua
Normal file
140
lua/autorun/includes/modules/htmlmaterial.lua
Normal 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
|
81
lua/autorun/mediaplayer.lua
Normal file
81
lua/autorun/mediaplayer.lua
Normal 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 )
|
3
lua/autorun/mediaplayer/cl_cvars.lua
Normal file
3
lua/autorun/mediaplayer/cl_cvars.lua
Normal 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 )
|
55
lua/autorun/mediaplayer/cl_idlescreen.lua
Normal file
55
lua/autorun/mediaplayer/cl_idlescreen.lua
Normal 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
|
254
lua/autorun/mediaplayer/cl_init.lua
Normal file
254
lua/autorun/mediaplayer/cl_init.lua
Normal 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 }) )
|
28
lua/autorun/mediaplayer/config/server.lua
Normal file
28
lua/autorun/mediaplayer/config/server.lua
Normal 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"
|
||||
}
|
||||
|
||||
})
|
349
lua/autorun/mediaplayer/controls/dhtmlcontrols.lua
Normal file
349
lua/autorun/mediaplayer/controls/dhtmlcontrols.lua
Normal 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" )
|
390
lua/autorun/mediaplayer/controls/dmediaplayerhtml.lua
Normal file
390
lua/autorun/mediaplayer/controls/dmediaplayerhtml.lua
Normal 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" )
|
128
lua/autorun/mediaplayer/controls/dmediaplayerrequest.lua
Normal file
128
lua/autorun/mediaplayer/controls/dmediaplayerrequest.lua
Normal 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" )
|
160
lua/autorun/mediaplayer/init.lua
Normal file
160
lua/autorun/mediaplayer/init.lua
Normal 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 )
|
15
lua/autorun/mediaplayer/players/_mixins/vote.lua
Normal file
15
lua/autorun/mediaplayer/players/_mixins/vote.lua
Normal 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:
|
171
lua/autorun/mediaplayer/players/base/cl_draw.lua
Normal file
171
lua/autorun/mediaplayer/players/base/cl_draw.lua
Normal 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
|
78
lua/autorun/mediaplayer/players/base/cl_fullscreen.lua
Normal file
78
lua/autorun/mediaplayer/players/base/cl_fullscreen.lua
Normal 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
|
140
lua/autorun/mediaplayer/players/base/cl_init.lua
Normal file
140
lua/autorun/mediaplayer/players/base/cl_init.lua
Normal 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 )
|
42
lua/autorun/mediaplayer/players/base/cl_net.lua
Normal file
42
lua/autorun/mediaplayer/players/base/cl_net.lua
Normal 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
|
490
lua/autorun/mediaplayer/players/base/init.lua
Normal file
490
lua/autorun/mediaplayer/players/base/init.lua
Normal 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
|
367
lua/autorun/mediaplayer/players/base/shared.lua
Normal file
367
lua/autorun/mediaplayer/players/base/shared.lua
Normal 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
|
27
lua/autorun/mediaplayer/players/base/sv_net.lua
Normal file
27
lua/autorun/mediaplayer/players/base/sv_net.lua
Normal 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
|
106
lua/autorun/mediaplayer/players/entity/cl_init.lua
Normal file
106
lua/autorun/mediaplayer/players/entity/cl_init.lua
Normal 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
|
10
lua/autorun/mediaplayer/players/entity/init.lua
Normal file
10
lua/autorun/mediaplayer/players/entity/init.lua
Normal 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
|
85
lua/autorun/mediaplayer/players/entity/sh_meta.lua
Normal file
85
lua/autorun/mediaplayer/players/entity/sh_meta.lua
Normal 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
|
89
lua/autorun/mediaplayer/players/entity/shared.lua
Normal file
89
lua/autorun/mediaplayer/players/entity/shared.lua
Normal 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
|
297
lua/autorun/mediaplayer/services/audiofile/cl_init.lua
Normal file
297
lua/autorun/mediaplayer/services/audiofile/cl_init.lua
Normal 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
|
112
lua/autorun/mediaplayer/services/audiofile/init.lua
Normal file
112
lua/autorun/mediaplayer/services/audiofile/init.lua
Normal 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
|
22
lua/autorun/mediaplayer/services/audiofile/shared.lua
Normal file
22
lua/autorun/mediaplayer/services/audiofile/shared.lua
Normal 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
|
33
lua/autorun/mediaplayer/services/base/cl_init.lua
Normal file
33
lua/autorun/mediaplayer/services/base/cl_init.lua
Normal 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
|
102
lua/autorun/mediaplayer/services/base/init.lua
Normal file
102
lua/autorun/mediaplayer/services/base/init.lua
Normal 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
|
191
lua/autorun/mediaplayer/services/base/shared.lua
Normal file
191
lua/autorun/mediaplayer/services/base/shared.lua
Normal 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
|
130
lua/autorun/mediaplayer/services/browser.lua
Normal file
130
lua/autorun/mediaplayer/services/browser.lua
Normal 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
|
31
lua/autorun/mediaplayer/services/googledrive/cl_init.lua
Normal file
31
lua/autorun/mediaplayer/services/googledrive/cl_init.lua
Normal 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
|
87
lua/autorun/mediaplayer/services/googledrive/init.lua
Normal file
87
lua/autorun/mediaplayer/services/googledrive/init.lua
Normal 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
|
58
lua/autorun/mediaplayer/services/googledrive/shared.lua
Normal file
58
lua/autorun/mediaplayer/services/googledrive/shared.lua
Normal 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
|
57
lua/autorun/mediaplayer/services/html5_video.lua
Normal file
57
lua/autorun/mediaplayer/services/html5_video.lua
Normal 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
|
23
lua/autorun/mediaplayer/services/image.lua
Normal file
23
lua/autorun/mediaplayer/services/image.lua
Normal 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
|
37
lua/autorun/mediaplayer/services/jwplayer.lua
Normal file
37
lua/autorun/mediaplayer/services/jwplayer.lua
Normal 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
|
16
lua/autorun/mediaplayer/services/resource/cl_init.lua
Normal file
16
lua/autorun/mediaplayer/services/resource/cl_init.lua
Normal 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
|
35
lua/autorun/mediaplayer/services/resource/init.lua
Normal file
35
lua/autorun/mediaplayer/services/resource/init.lua
Normal 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
|
21
lua/autorun/mediaplayer/services/resource/shared.lua
Normal file
21
lua/autorun/mediaplayer/services/resource/shared.lua
Normal 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
|
15
lua/autorun/mediaplayer/services/shoutcast.lua
Normal file
15
lua/autorun/mediaplayer/services/shoutcast.lua
Normal 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
|
1
lua/autorun/mediaplayer/services/soundcloud/cl_init.lua
Normal file
1
lua/autorun/mediaplayer/services/soundcloud/cl_init.lua
Normal file
@ -0,0 +1 @@
|
||||
include "shared.lua"
|
104
lua/autorun/mediaplayer/services/soundcloud/init.lua
Normal file
104
lua/autorun/mediaplayer/services/soundcloud/init.lua
Normal 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
|
20
lua/autorun/mediaplayer/services/soundcloud/shared.lua
Normal file
20
lua/autorun/mediaplayer/services/soundcloud/shared.lua
Normal 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
|
56
lua/autorun/mediaplayer/services/twitch/cl_init.lua
Normal file
56
lua/autorun/mediaplayer/services/twitch/cl_init.lua
Normal 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
|
92
lua/autorun/mediaplayer/services/twitch/init.lua
Normal file
92
lua/autorun/mediaplayer/services/twitch/init.lua
Normal 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
|
54
lua/autorun/mediaplayer/services/twitch/shared.lua
Normal file
54
lua/autorun/mediaplayer/services/twitch/shared.lua
Normal 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
|
23
lua/autorun/mediaplayer/services/twitchstream/cl_init.lua
Normal file
23
lua/autorun/mediaplayer/services/twitchstream/cl_init.lua
Normal 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
|
61
lua/autorun/mediaplayer/services/twitchstream/init.lua
Normal file
61
lua/autorun/mediaplayer/services/twitchstream/init.lua
Normal 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
|
44
lua/autorun/mediaplayer/services/twitchstream/shared.lua
Normal file
44
lua/autorun/mediaplayer/services/twitchstream/shared.lua
Normal 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
|
49
lua/autorun/mediaplayer/services/vimeo/cl_init.lua
Normal file
49
lua/autorun/mediaplayer/services/vimeo/cl_init.lua
Normal 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
|
68
lua/autorun/mediaplayer/services/vimeo/init.lua
Normal file
68
lua/autorun/mediaplayer/services/vimeo/init.lua
Normal 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
|
38
lua/autorun/mediaplayer/services/vimeo/shared.lua
Normal file
38
lua/autorun/mediaplayer/services/vimeo/shared.lua
Normal 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
|
179
lua/autorun/mediaplayer/services/youtube/cl_init.lua
Normal file
179
lua/autorun/mediaplayer/services/youtube/cl_init.lua
Normal 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
|
149
lua/autorun/mediaplayer/services/youtube/init.lua
Normal file
149
lua/autorun/mediaplayer/services/youtube/init.lua
Normal 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
|
79
lua/autorun/mediaplayer/services/youtube/shared.lua
Normal file
79
lua/autorun/mediaplayer/services/youtube/shared.lua
Normal 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
|
105
lua/autorun/mediaplayer/sh_history.lua
Normal file
105
lua/autorun/mediaplayer/sh_history.lua
Normal 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
|
198
lua/autorun/mediaplayer/sh_mediaplayer.lua
Normal file
198
lua/autorun/mediaplayer/sh_mediaplayer.lua
Normal 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" )
|
127
lua/autorun/mediaplayer/sh_services.lua
Normal file
127
lua/autorun/mediaplayer/sh_services.lua
Normal 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
|
78
lua/autorun/mediaplayer/shared.lua
Normal file
78
lua/autorun/mediaplayer/shared.lua
Normal 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)
|
174
lua/autorun/mediaplayer/sv_metadata.lua
Normal file
174
lua/autorun/mediaplayer/sv_metadata.lua
Normal 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
|
8
lua/autorun/menubar/mp_options.lua
Normal file
8
lua/autorun/menubar/mp_options.lua
Normal 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 )
|
185
lua/autorun/properties/mediaplayer.lua
Normal file
185
lua/autorun/properties/mediaplayer.lua
Normal 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
|
||||
})
|
84
lua/entities/mediaplayer_base/shared.lua
Normal file
84
lua/entities/mediaplayer_base/shared.lua
Normal 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
|
20
lua/entities/mediaplayer_example/shared.lua
Normal file
20
lua/entities/mediaplayer_example/shared.lua
Normal 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
|
||||
}
|
86
lua/entities/mediaplayer_projector/shared.lua
Normal file
86
lua/entities/mediaplayer_projector/shared.lua
Normal 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
|
8
mediaplayer.sublime-project
Normal file
8
mediaplayer.sublime-project
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"folders":
|
||||
[
|
||||
{
|
||||
"path": "."
|
||||
}
|
||||
]
|
||||
}
|
BIN
resource/fonts/ClearSans-Medium.ttf
Normal file
BIN
resource/fonts/ClearSans-Medium.ttf
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user