├── .babelrc
├── .gitignore
├── README.md
├── dist
└── index.js
├── index.js
├── package.json
├── src
├── css
│ ├── playerComponent.css
│ └── videojs-resolution-switcher.css
├── index.js
└── js
│ ├── videojs-contrib-hls.js
│ └── videojs-resolution-switcher.js
└── webpack.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["env"],
3 | "plugins": ["transform-object-rest-spread","transform-react-jsx"]
4 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | /package-lock.json
3 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # VIDEOJS FRO REACT
2 |
3 | 该项目是一个react组件,组件封装了video.js。并集成了部分拓展功能,如:多码流切换,对HLS流的支持。
4 |
5 | ## 使用方法
6 |
7 | 在项目中使用npm或者yarn安装依赖:
8 |
9 | `npm install --save videojs-fro-react` 或者 `yarn add videojf-fro-react`
10 |
11 | 该组件是针对video.js的封装,支持video.js的所有设置。具体设置请参考[ video.js文档](http://docs.videojs.com/)及[video.js github](https://github.com/videojs/video.js)。
12 | 以下仅仅展现部分设置,以及该组件所添加的API。
13 |
14 | 示例代码:
15 |
16 | ```
17 |
18 | import React, { Component } from 'react';
19 | import VideoJsForReact from 'videojs-for-react';
20 |
21 | class App extends Component {
22 | constructor() {
23 | super()
24 | this.state = {
25 | videoJsOptions: {
26 | preload: 'auto', // 预加载
27 | bigPlayButton: {}, // 大按钮
28 | autoplay: true, // 自动播放
29 | controls: true, // 是否开启控制栏
30 | width: 800, // 播放器宽度
31 | height: 600, // 播放器高度
32 | playbackRates: [1, 1.5, 2], // 播放倍速
33 | sources: [ // 视频源
34 | {
35 | src: 'http://yunxianchang.live.ujne7.com/vod-system-bj/44_176_20170224113626af3a75cd-3508-4bc3-b51f-366fca3c7e39.m3u8',
36 | type: 'application/x-mpegURL',
37 | label: 'HLS1',
38 | withCredentials: false,
39 | res: 960
40 | }, {
41 | src: 'http://192.168.199.197:5000/nodeJS%E8%A7%86%E9%A2%914.mp4',
42 | type: 'video/mp4',
43 | label: 'MP4',
44 | res: 1080
45 | }
46 | ]
47 | }
48 | }
49 | }
50 |
51 | render() {
52 | return (
53 |
54 | console.log(player)}
56 | onReady={(player) => console.log('准备完毕', player)}
57 | {...this.state.videoJsOptions}
58 | >
59 |
60 |
61 | )
62 | }
63 | }
64 |
65 | export default App;
66 |
67 | ```
68 |
69 | 与video.js不同的是,videojs-fro-react对接收的资源做了一些拓展:
70 |
71 | ```
72 |
73 | // 字段sources为array类型,当sources.length为1时,代表只播放一路码流,不开启视频源切换。
74 | // 当字段sources.length>1时,播放器将会开启多码流切换功能。
75 | sources: [
76 | {
77 | src: 'http://yunxianchang.live.ujne7.com/vod-system-bj/44_176_20170224113626af3a75cd-3508-4bc3-b51f-366fca3c7e39.m3u8',
78 | type: 'application/x-mpegURL', // MIME类型
79 | label: 'HLS1', // 代表该路码流名称
80 | res: 960 // 清晰度
81 | }, {
82 | src: 'http://192.168.199.197:5000/nodeJS%E8%A7%86%E9%A2%914.mp4',
83 | type: 'video/mp4',
84 | label: 'MP4',
85 | res: 1080
86 | }
87 | ]
88 | ```
89 |
90 | ## API
91 |
92 | | API | 用途|
93 | | - | -: |
94 | | sourceChanged| 码流切换成功的回调 |
95 | | onReady| 播放器准备就绪的回调 |
96 |
97 | 现阶段该组件并没有实现更多功能的封装,接下来将会考虑支持rtmp、http-flv等。希望能够提出宝贵的建议,大家一起完善组件的功能。
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | module.exports = require('./dist/index');
2 | // "build": "cross-env NODE_ENV=production webpack --config webpack.config.js --mode=production"
3 | // "build": "webpack --config webpack.config.js"
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "videojs-for-react",
3 | "version": "0.0.3",
4 | "description": "videojs-for-react,This is react component",
5 | "main": "index.js",
6 | "files": [
7 | "dist",
8 | "index.js",
9 | "package.json"
10 | ],
11 | "scripts": {
12 | "test": "echo \"Error: no test specified\" && exit 1",
13 | "build": "cross-env NODE_ENV=production webpack --config webpack.config.js --mode=production"
14 | },
15 | "repository": {
16 | "type": "git",
17 | "url": "git+https://github.com/sohoorc/videojs-for-react.git"
18 | },
19 | "keywords": [
20 | "videojs",
21 | "videojsForReact",
22 | "videojs-react",
23 | "videojs-fro-react",
24 | "videojsReact"
25 | ],
26 | "author": "WeiTian Gan",
27 | "license": "ISC",
28 | "bugs": {
29 | "url": "https://github.com/sohoorc/videojs-for-react/issues"
30 | },
31 | "homepage": "https://github.com/sohoorc/videojs-for-react#readme",
32 | "dependencies": {
33 | "prop-types": "^15.6.1",
34 | "react": "^16.3.2",
35 | "react-dom": "^16.3.2",
36 | "video.js": "^6.9.0",
37 | "videojs-contrib-hls": "^5.14.1"
38 | },
39 | "devDependencies": {
40 | "babel-core": "^6.26.3",
41 | "babel-loader": "^7.1.4",
42 | "babel-plugin-transform-object-rest-spread": "^6.26.0",
43 | "babel-plugin-transform-react-jsx": "^6.24.1",
44 | "babel-preset-env": "^1.7.0",
45 | "cross-env": "^5.1.5",
46 | "css-loader": "^0.28.11",
47 | "extract-text-webpack-plugin": "^3.0.2",
48 | "prop-types": "^15.6.1",
49 | "style-loader": "^0.21.0",
50 | "url-loader": "^1.0.1",
51 | "webpack": "^3.x",
52 | "webpack-cli": "^2.1.3"
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/css/playerComponent.css:
--------------------------------------------------------------------------------
1 | @import './videojs-resolution-switcher.css';
2 |
3 | .vjs-menu-button-popup div.vjs-menu{
4 | left: 0;
5 | }
--------------------------------------------------------------------------------
/src/css/videojs-resolution-switcher.css:
--------------------------------------------------------------------------------
1 | .vjs-resolution-button {
2 | color: #ccc;
3 | font-family: VideoJS;
4 | }
5 |
6 | .vjs-resolution-button .vjs-resolution-button-staticlabel:before {
7 | content: '\f110';
8 | font-size: 1.8em;
9 | line-height: 1.67;
10 | }
11 |
12 | .vjs-resolution-button .vjs-resolution-button-label {
13 | font-size: 1em;
14 | line-height: 3em;
15 | position: absolute;
16 | top: 0;
17 | left: 0;
18 | width: 100%;
19 | height: 100%;
20 | text-align: center;
21 | box-sizing: inherit;
22 | font-family: Arial, Helvetica, sans-serif;
23 | }
24 |
25 | .vjs-resolution-button ul.vjs-menu-content {
26 | width: 4em !important;
27 | }
28 |
29 | .vjs-resolution-button .vjs-menu {
30 | left: 0;
31 | }
32 |
33 | .vjs-resolution-button .vjs-menu li {
34 | text-transform: none;
35 | font-size: 1em;
36 | font-family: Arial, Helvetica, sans-serif;
37 | }
38 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import PropTypes from 'prop-types';
3 | import videojs from 'video.js';
4 | import { HlsSourceHandler } from 'videojs-contrib-hls';
5 | import './js/videojs-resolution-switcher';
6 | import 'video.js/dist/video-js.min.css';
7 | import './css/playerComponent.css';
8 | // import Swf from 'videojs-swf/dist/video-js.swf';
9 | // import 'videojs-flash';
10 | // import 'videojs-resolution-switcher';
11 |
12 | export default class VideoJsForReact extends Component {
13 | constructor(props) {
14 | super(props);
15 | this.options = {}
16 | this.sources = [];
17 | // 判断是否是多码流,来修改播放器的播放方式
18 | if (props.sources.length > 1) {
19 | // 若存在多个流地址,则开启videoJsResolutionSwitcher
20 | this.sources = props.sources
21 | } else {
22 | this.options.source = props.sources
23 | }
24 | }
25 |
26 | componentDidMount() {
27 | videojs.getTech('html5').registerSourceHandler(HlsSourceHandler('html5'), 0);
28 | let _this = this
29 | this.player = videojs(this.videoContainer, {
30 | ...this.props,
31 | ...this.options,
32 | plugins: {
33 | videoJsResolutionSwitcher: {
34 | default: 'low', // Default resolution [{Number}, 'low', 'high'],
35 | dynamicLabel: true // Display dynamic labels or gear symbol
36 | }
37 | }
38 | // flash: {
39 | // swf: Swf,
40 | // },
41 | }, function () {
42 | let player = this
43 | let props = _this.props
44 | let sources = _this.sources
45 |
46 | // 播放器加载成功的回调
47 | if (!!props.onReady) {
48 | props.onReady(props)
49 | }
50 |
51 | // 修正使用多码流播放时自动播放失效的BUG
52 | // if (!!props.autoplay) {
53 | // setInterval(() => {
54 | // player.play();
55 | // }, 100)
56 | // }
57 |
58 | // 判断是否是多码流,单码流调用video.js播放器播放,多码流使用插件播放
59 | if (sources.length > 1) {
60 | player.updateSrc([...sources])
61 | player.on('resolutionchange', function () {
62 | // 切换成功的回调
63 | if (!!props.sourceChanged) {
64 | props.sourceChanged(player)
65 | }
66 | })
67 | }
68 | })
69 | }
70 |
71 | componentWillUnmount() {
72 | // 销毁播放器
73 | if (this.player) {
74 | this.player.dispose()
75 | }
76 | }
77 |
78 | render() {
79 | return (
80 |
81 |
85 |
86 | )
87 | }
88 | }
89 |
90 | VideoJsForReact.propTypes = {
91 | sources: PropTypes.array.isRequired, // 视频流地址列表 数组,必填
92 | sourceChanged: PropTypes.func, // 多码流时,对码流切换的回调
93 | onReady: PropTypes.func // 播放器加载成功时的回调
94 | }
95 |
96 |
--------------------------------------------------------------------------------
/src/js/videojs-resolution-switcher.js:
--------------------------------------------------------------------------------
1 | /*! videojs-resolution-switcher - 2015-7-26
2 | * Copyright (c) 2016 Kasper Moskwiak
3 | * Modified by Pierre Kraft
4 | * Licensed under the Apache-2.0 license. */
5 |
6 | (function() {
7 | /* jshint eqnull: true*/
8 | /* global require */
9 | 'use strict';
10 | var videojs = null;
11 | if(typeof window.videojs === 'undefined' && typeof require === 'function') {
12 | videojs = require('video.js');
13 | } else {
14 | videojs = window.videojs;
15 | }
16 |
17 | (function(window, videojs) {
18 |
19 |
20 | var defaults = {},
21 | videoJsResolutionSwitcher,
22 | currentResolution = {}, // stores current resolution
23 | menuItemsHolder = {}; // stores menuItems
24 |
25 | function setSourcesSanitized(player, sources, label, customSourcePicker) {
26 | currentResolution = {
27 | label: label,
28 | sources: sources
29 | };
30 | if(typeof customSourcePicker === 'function'){
31 | return customSourcePicker(player, sources, label);
32 | }
33 | /* DECEiFER: Modified for video.js 6.x */
34 | player.src(sources.map(function(src) {
35 | return {src: src.src, type: src.type, res: src.res};
36 | }));
37 | return player;
38 | }
39 |
40 | /*
41 | * Resolution menu item
42 | */
43 | var MenuItem = videojs.getComponent('MenuItem');
44 | var ResolutionMenuItem = videojs.extend(MenuItem, {
45 | constructor: function(player, options, onClickListener, label){
46 | this.onClickListener = onClickListener;
47 | this.label = label;
48 | // Sets this.player_, this.options_ and initializes the component
49 | MenuItem.call(this, player, options);
50 | this.src = options.src;
51 |
52 | this.on('click', this.onClick);
53 | this.on('touchstart', this.onClick);
54 |
55 | if (options.initialySelected) {
56 | this.showAsLabel();
57 | this.selected(true);
58 |
59 | this.addClass('vjs-selected');
60 | }
61 | },
62 | showAsLabel: function() {
63 | // Change menu button label to the label of this item if the menu button label is provided
64 | if(this.label) {
65 | this.label.innerHTML = this.options_.label;
66 | }
67 | },
68 | onClick: function(customSourcePicker){
69 | this.onClickListener(this);
70 | // Remember player state
71 | var currentTime = this.player_.currentTime();
72 | var isPaused = this.player_.paused();
73 | this.showAsLabel();
74 |
75 | // add .current class
76 | this.addClass('vjs-selected');
77 |
78 | // Hide bigPlayButton
79 | if(!isPaused){
80 | this.player_.bigPlayButton.hide();
81 | }
82 | if(typeof customSourcePicker !== 'function' &&
83 | typeof this.options_.customSourcePicker === 'function'){
84 | customSourcePicker = this.options_.customSourcePicker;
85 | }
86 | // Change player source and wait for loadeddata event, then play video
87 | // loadedmetadata doesn't work right now for flash.
88 | // Probably because of https://github.com/videojs/video-js-swf/issues/124
89 | // If player preload is 'none' and then loadeddata not fired. So, we need timeupdate event for seek handle (timeupdate doesn't work properly with flash)
90 | var handleSeekEvent = 'loadeddata';
91 | if(this.player_.techName_ !== 'Youtube' && this.player_.preload() === 'none' && this.player_.techName_ !== 'Flash') {
92 | handleSeekEvent = 'timeupdate';
93 | }
94 | setSourcesSanitized(this.player_, this.src, this.options_.label, customSourcePicker).one(handleSeekEvent, function() {
95 | this.player_.currentTime(currentTime);
96 | this.player_.handleTechSeeked_();
97 | if(!isPaused){
98 | // Start playing and hide loadingSpinner (flash issue ?)
99 | /* DECEiFER: Modified for video.js 6.x */
100 | this.player_.play();
101 | this.player_.handleTechSeeked_();
102 | }
103 | this.player_.trigger('resolutionchange');
104 | });
105 | }
106 | });
107 |
108 |
109 | /*
110 | * Resolution menu button
111 | */
112 | var MenuButton = videojs.getComponent('MenuButton');
113 | var ResolutionMenuButton = videojs.extend(MenuButton, {
114 | constructor: function(player, options, settings, label){
115 | this.sources = options.sources;
116 | this.label = label;
117 | this.label.innerHTML = options.initialySelectedLabel;
118 | // Sets this.player_, this.options_ and initializes the component
119 | MenuButton.call(this, player, options, settings);
120 | this.controlText('Quality');
121 |
122 | if(settings.dynamicLabel){
123 | this.el().appendChild(label);
124 | }else{
125 | var staticLabel = document.createElement('span');
126 | /* DECEiFER: Modified for video.js 6.x */
127 | videojs.dom.addClass(staticLabel, 'vjs-resolution-button-staticlabel');
128 | this.el().appendChild(staticLabel);
129 | }
130 | },
131 | createItems: function(){
132 | var menuItems = [];
133 | var labels = (this.sources && this.sources.label) || {};
134 | var onClickUnselectOthers = function(clickedItem) {
135 | menuItems.map(function(item) {
136 | item.selected(item === clickedItem);
137 | item.removeClass('vjs-selected');
138 | });
139 | };
140 |
141 | for (var key in labels) {
142 | if (labels.hasOwnProperty(key)) {
143 | menuItems.push(new ResolutionMenuItem(
144 | this.player_,
145 | {
146 | label: key,
147 | src: labels[key],
148 | initialySelected: key === this.options_.initialySelectedLabel,
149 | customSourcePicker: this.options_.customSourcePicker
150 | },
151 | onClickUnselectOthers,
152 | this.label));
153 | // Store menu item for API calls
154 | menuItemsHolder[key] = menuItems[menuItems.length - 1];
155 | }
156 | }
157 | return menuItems;
158 | }
159 | });
160 |
161 | /**
162 | * Initialize the plugin.
163 | * @param {object} [options] configuration for the plugin
164 | */
165 | videoJsResolutionSwitcher = function(options) {
166 | var settings = videojs.mergeOptions(defaults, options),
167 | player = this,
168 | label = document.createElement('span'),
169 | groupedSrc = {};
170 | /* DECEiFER: Modified for video.js 6.x */
171 | videojs.dom.addClass(label, 'vjs-resolution-button-label');
172 |
173 | /**
174 | * Updates player sources or returns current source URL
175 | * @param {Array} [src] array of sources [{src: '', type: '', label: '', res: ''}]
176 | * @returns {Object|String|Array} videojs player object if used as setter or current source URL, object, or array of sources
177 | */
178 | player.updateSrc = function(src){
179 | //Return current src if src is not given
180 | if(!src){ return player.src(); }
181 | // Dispose old resolution menu button before adding new sources
182 | if(player.controlBar.resolutionSwitcher){
183 | player.controlBar.resolutionSwitcher.dispose();
184 | delete player.controlBar.resolutionSwitcher;
185 | }
186 | //Sort sources
187 | src = src.sort(compareResolutions);
188 | groupedSrc = bucketSources(src);
189 | var choosen = chooseSrc(groupedSrc, src);
190 | var menuButton = new ResolutionMenuButton(player, { sources: groupedSrc, initialySelectedLabel: choosen.label , initialySelectedRes: choosen.res , customSourcePicker: settings.customSourcePicker}, settings, label);
191 | /* DECEiFER: Modified for video.js 6.x */
192 | videojs.dom.addClass(menuButton.el(), 'vjs-resolution-button');
193 | player.controlBar.resolutionSwitcher = player.controlBar.el_.insertBefore(menuButton.el_, player.controlBar.getChild('fullscreenToggle').el_);
194 | player.controlBar.resolutionSwitcher.dispose = function(){
195 | this.parentNode.removeChild(this);
196 | };
197 | return setSourcesSanitized(player, choosen.sources, choosen.label);
198 | };
199 |
200 | /**
201 | * Returns current resolution or sets one when label is specified
202 | * @param {String} [label] label name
203 | * @param {Function} [customSourcePicker] custom function to choose source. Takes 3 arguments: player, sources, label. Must return player object.
204 | * @returns {Object} current resolution object {label: '', sources: []} if used as getter or player object if used as setter
205 | */
206 | player.currentResolution = function(label, customSourcePicker){
207 | if(label == null) { return currentResolution; }
208 | if(menuItemsHolder[label] != null){
209 | menuItemsHolder[label].onClick(customSourcePicker);
210 | }
211 | return player;
212 | };
213 |
214 | /**
215 | * Returns grouped sources by label, resolution and type
216 | * @returns {Object} grouped sources: { label: { key: [] }, res: { key: [] }, type: { key: [] } }
217 | */
218 | player.getGroupedSrc = function(){
219 | return groupedSrc;
220 | };
221 |
222 | /**
223 | * Method used for sorting list of sources
224 | * @param {Object} a - source object with res property
225 | * @param {Object} b - source object with res property
226 | * @returns {Number} result of comparation
227 | */
228 | function compareResolutions(a, b){
229 | if(!a.res || !b.res){ return 0; }
230 | return (+b.res)-(+a.res);
231 | }
232 |
233 | /**
234 | * Group sources by label, resolution and type
235 | * @param {Array} src Array of sources
236 | * @returns {Object} grouped sources: { label: { key: [] }, res: { key: [] }, type: { key: [] } }
237 | */
238 | function bucketSources(src){
239 | var resolutions = {
240 | label: {},
241 | res: {},
242 | type: {}
243 | };
244 | src.map(function(source) {
245 | initResolutionKey(resolutions, 'label', source);
246 | initResolutionKey(resolutions, 'res', source);
247 | initResolutionKey(resolutions, 'type', source);
248 |
249 | appendSourceToKey(resolutions, 'label', source);
250 | appendSourceToKey(resolutions, 'res', source);
251 | appendSourceToKey(resolutions, 'type', source);
252 | });
253 | return resolutions;
254 | }
255 |
256 | function initResolutionKey(resolutions, key, source) {
257 | if(resolutions[key][source[key]] == null) {
258 | resolutions[key][source[key]] = [];
259 | }
260 | }
261 |
262 | function appendSourceToKey(resolutions, key, source) {
263 | resolutions[key][source[key]].push(source);
264 | }
265 |
266 | /**
267 | * Choose src if option.default is specified
268 | * @param {Object} groupedSrc {res: { key: [] }}
269 | * @param {Array} src Array of sources sorted by resolution used to find high and low res
270 | * @returns {Object} {res: string, sources: []}
271 | */
272 | function chooseSrc(groupedSrc, src){
273 | var selectedRes = settings['default']; // use array access as default is a reserved keyword
274 | var selectedLabel = '';
275 | if (selectedRes === 'high') {
276 | selectedRes = src[0].res;
277 | selectedLabel = src[0].label;
278 | } else if (selectedRes === 'low' || selectedRes == null || !groupedSrc.res[selectedRes]) {
279 | // Select low-res if default is low or not set
280 | selectedRes = src[src.length - 1].res;
281 | selectedLabel = src[src.length -1].label;
282 | } else if (groupedSrc.res[selectedRes]) {
283 | selectedLabel = groupedSrc.res[selectedRes][0].label;
284 | }
285 |
286 | return {res: selectedRes, label: selectedLabel, sources: groupedSrc.res[selectedRes]};
287 | }
288 |
289 | function initResolutionForYt(player){
290 | // Init resolution
291 | player.tech_.ytPlayer.setPlaybackQuality('default');
292 |
293 | // Capture events
294 | player.tech_.ytPlayer.addEventListener('onPlaybackQualityChange', function(){
295 | player.trigger('resolutionchange');
296 | });
297 |
298 | // We must wait for play event
299 | player.one('play', function(){
300 | var qualities = player.tech_.ytPlayer.getAvailableQualityLevels();
301 | // Map youtube qualities names
302 | var _yts = {
303 | highres: {res: 1080, label: '1080', yt: 'highres'},
304 | hd1080: {res: 1080, label: '1080', yt: 'hd1080'},
305 | hd720: {res: 720, label: '720', yt: 'hd720'},
306 | large: {res: 480, label: '480', yt: 'large'},
307 | medium: {res: 360, label: '360', yt: 'medium'},
308 | small: {res: 240, label: '240', yt: 'small'},
309 | tiny: {res: 144, label: '144', yt: 'tiny'},
310 | auto: {res: 0, label: 'auto', yt: 'default'}
311 | };
312 |
313 | var _sources = [];
314 |
315 | qualities.map(function(q){
316 | _sources.push({
317 | src: player.src().src,
318 | type: player.src().type,
319 | label: _yts[q].label,
320 | res: _yts[q].res,
321 | _yt: _yts[q].yt
322 | });
323 | });
324 |
325 | groupedSrc = bucketSources(_sources);
326 |
327 | // Overwrite defualt sourcePicer function
328 | var _customSourcePicker = function(_player, _sources, _label){
329 | player.tech_.ytPlayer.setPlaybackQuality(_sources[0]._yt);
330 | return player;
331 | };
332 |
333 | var choosen = {label: 'auto', res: 0, sources: groupedSrc.label.auto};
334 | var menuButton = new ResolutionMenuButton(player, {
335 | sources: groupedSrc,
336 | initialySelectedLabel: choosen.label,
337 | initialySelectedRes: choosen.res,
338 | customSourcePicker: _customSourcePicker
339 | }, settings, label);
340 |
341 | menuButton.el().classList.add('vjs-resolution-button');
342 | player.controlBar.resolutionSwitcher = player.controlBar.addChild(menuButton);
343 | });
344 | }
345 |
346 | player.ready(function(){
347 | if(player.options_.sources.length > 1){
348 | // tech: Html5 and Flash
349 | // Create resolution switcher for videos form tag inside