数据专栏

智能大数据搬运工,你想要的我们都有

科技资讯:

科技学院:

科技百科:

科技书籍:

网站大全:

软件大全:

「深度学习福利」大神带你进阶工程师,立即查看>>>
当下直播行业正在如火如途进行中,但是我们对直播所使用到的技术,应该有所了解。
直播有三个阶段分别为建流、推流、拉流
建流:通常rtmp协议用 rtmp 模块实现的,这是一个nginx第三方模块很强大,很多第三方的直播SDK应该都是基于其实现的。
推流:就是主播端用到的
拉流:就是观看用户用的
推送和拉取用 ffmpeg 开源软件就可以做到,只不过界面有点简陋,所以第三方厂商对其也进行了封装成了SDK。
众所周知,直播对于带宽消耗很大,虽然服务端和客户端可以自建,但是旁大的带宽消耗是大部分企业无法承受的,所以选择第三方SDK是最好的归宿,因为他们有高额带宽、有CDN、有超大存储。

ffmpeg 简单用法
高质量录屏
ffmpeg -f gdigrab -i desktop -preset ultrafast -crf 10 playback.mp4
无损录屏
ffmpeg -video_size 1920x1080 -framerate 30 -f gdigrab -i desktop -c:v libx264 -qp 0 -preset ultrafast playback .mp4
直播
ffmpeg -f gdigrab -s 500x300 -i desktop -c:v libx264 -b:v 2M -qp 0 -crf 10 -preset ultrafast -f rtsp -rtsp_transport tcp rtsp://localhost:2333/live.sdp

ffplay -rtsp_flags listen rtsp://localhost:2333/live.sdp?tcp

多媒体
2020-03-28 11:51:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
本教学技法画书内容讲解理论精炼,达到言简意赅,图例色彩丰富能使学习者更好地掌握美术基础知识。本教学技法画书系统地讲解山水画技法,包括山石技法、树木技法、点景技法、人物技法、人物技法、动物技法等等,这样可使学习者由简至繁、由浅至深地把整个人生情思和艺术素养一并提升到比较高的层次。
本教学技法画书适合广大美术爱好者和业余美术设计爱好者学习,也授予广大大专院校以教学科本使用并与学生学习,也适合广大专业美术工作者与高级3D美术设计名师作参考材料使用。
【叶英伦原创编著教学技法画书出版时间信息】
叶英伦彩墨山水画国画技法图书照片 书名:《叶英伦彩墨山水画国画技法》
作 者:叶英伦 编著
出 版 社:岭南美术出版社
出 版 时 间:2014年8月
版 次:2014年8月第1版
印 次:2014年8月第1次印刷
开 本:889mm×1194mm 1/12
印 张:15
印 数:1—1000册
外 文 名:YEYINGLUN
CAIMOSHANSHUIHUAZUOPINJINGXUAN
I S B N:978—7—5362—5508—1
定 价:160.00yua

叶英伦原创编著写实景教学技法出版画书《叶英伦彩墨山水画国画技法》
一本由国际著名书画家叶英伦原创编著写实景色教学技法画书并由岭南美术出版社出版与全国各大新华书馆发行的教学技法画书。本教学技法画书入选国家级艺术书画名家名录国际图书馆收藏并得到各个艺术界名人专家与学者欢迎关注交流赏析收藏。
《叶英伦彩墨山水画国画》
《叶英伦彩墨山水画国画》
《叶英伦彩墨山水画国画》
叶英伦原创代表作品赏析
《美景逸趣图》
《雅景骁骥闲》
《闪烁千澜足威英》
《驰骋旷阔天》
《碧水消夏图》
《矞云瑞雪迎美虎》
《源河伴翠》
《源河伴翠坪》
作者:叶英伦
国际著名书画家叶英伦 岭南画派 国际明星 美术设计知名度知心高级讲师









多媒体
2020-03-28 09:07:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
伴随疫情突发,在线教育、视频会议、互动直播与在线视频需求被推上风口,如何在大规模、高并发视频需求下为用户提供更流畅、更高清、低延迟的直播与视频观看体验?如何为线上教育赋予电子白板功能优化用户学习体验与教学效果?如何激发用户在线视频观看兴趣、提升用户视频交互体验?

3月29日19:00~21:30 , LiveVideoStack 携手 UCloud RTC首席架构师王立飞 、 学而思网校资深架构师赵文杰 、 哔哩哔哩资深研发工程师唐君行 , 共同探索大规模互动直播与在线视频背后的底层技术架构,分享用户体验优化实践经验。
《URTC万人连麦直播的优化实践》 王立飞 UCloud RTC首席架构师 《线上/线下教育中白板技术优化》 赵文杰 学而思网校资深架构师 《高能进度条:视频交互体验优化》 唐君行 哔哩哔哩资深研发工程师

扫描下图 二维码 或访问 : http://mudu.tv/?c=activity&a=live&id=309460 预约直播。

多媒体
2020-03-27 12:52:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
AdePlayer win1.0版本,使用免费,根据you-get分析网址内容获得真实地址,使用aria2下载各主流网站的媒体文件,可播放欣赏,然后转换格式,提取音频。
更多详情http://t.cn/A6ZUj2VP
1 Analyse,分析网页,使用you-get分析各大媒体的网址,获得下载媒体的真实地址;
2 Download,下载内容,使用aria2下载,不限速;
3 Play,播放视频或音乐;
4 Extract,提取转换格式。
注册后,没有提示框。
支持网站
163,网易视频,网易云音乐,56网,AcFun,Baidu,百度贴吧,爆米花网,bilibili,哔哩哔哩,豆瓣,斗鱼,凤凰视频,风行网,iQIYI,爱奇艺,激动网,酷6网,酷狗音乐,酷我音乐,乐视网,荔枝FM,秒拍,MioMio弹幕网,MissEvan,猫耳FM,痞客邦,PPTV聚力,齐鲁网,QQ,腾讯视频,企鹅直播,Sina,新浪视频,微博秒拍视频,Sohu,搜狐视频,Tudou,土豆,虾米,阳光卫视,音悦Tai,Youku,优酷,战旗TV,央视网,Naver,네이버,芒果TV,火猫TV,阳光宽频网,西瓜视频,快手,抖音,TikTok,中国体育(TV),知乎
YouTube,Twitter,VK,Vine,Vimeo,Veoh,Tumblr,TED,SoundCloud,SHOWROOM,Pinterest,MTV81,Mixcloud,Metacafe,Magisto,Khan Academy,Internet Archive,Instagram,InfoQ,Imgur,Heavy Music Archive,Freesound,Flickr,FC2 Video,Facebook,eHow,Dailymotion,Coub,CBS,Bandcamp,AliveThai,interest.me,755,ナナゴーゴー,niconico,ニコニコ動画
BdePlayer win1.1 为更新版本
AdePlayer win1.0
Github 下载地址 gitee下载 http://t.cn/A6ZUj2ce
百度网盘 https://pan.baidu.com/s/1ByZFSZYrBk7MSdL1VpajPg 提取码:cuhw


Adeplayer winv1.0, It's Free,according to the analysis of websites by you-get, download the media files of mainstream websites with aria2,
and you can play and enjoy it,then convert different formats and extract the audio file.
More details http://t.cn/a6zuj2vp
1 Analyze, analyse web pages of mainstream websites by you-get tool to get the real addresses of download media;
2 Download, download content with aria2,no speed limit;
3 Play, play video or music;
4 Extract, extract different format.
After registration, there is no prompt box.
Supported Sites:
YouTube,Twitter,VK,Vine,Vimeo,Veoh,Tumblr,TED,SoundCloud,SHOWROOM,Pinterest,MTV81,Mixcloud,Metacafe,Magisto,Khan Academy,Internet Archive,Instagram,InfoQ,Imgur,Heavy Music Archive,Freesound,Flickr,FC2 Video,Facebook,eHow,Dailymotion,Coub,CBS,Bandcamp,AliveThai,interest.me,755,ナナゴーゴー,niconico,ニコニコ動画
163,网易视频,网易云音乐,56网,AcFun,Baidu,百度贴吧,爆米花网,bilibili,哔哩哔哩,豆瓣,斗鱼,凤凰视频,风行网,iQIYI,爱奇艺,激动网,酷6网,酷狗音乐,酷我音乐,乐视网,荔枝FM,秒拍,MioMio弹幕网,MissEvan,猫耳FM,痞客邦,PPTV聚力,齐鲁网,QQ,腾讯视频,企鹅直播,Sina,新浪视频,微博秒拍视频,Sohu,搜狐视频,Tudou,土豆,虾米,阳光卫视,音悦Tai,Youku,优酷,战旗TV,央视网,Naver,네이버,芒果TV,火猫TV,阳光宽频网,西瓜视频,快手,抖音,TikTok,中国体育(TV),知乎

多媒体
2020-03-26 20:25:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
在拷贝到txt时,需要注意保存为unicode格式。
Unicode字符百科:https://unicode-table.com/cn/#control-character
常用字符 , 、 。 . ? ! ~ $ % @ & # * ? ; ︰ … ‥ ﹐ ﹒ ˙ ? ‘ ’ “ ” 〝 〞 ‵ ′ 〃 ↑ ↓ ← → ↖ ↗ ↙ ↘ ㊣ ◎ ○ ● ⊕ ⊙ ○ ● △ ▲ ☆ ★ ◇ ◆ □ ■ ▽ ▼ § ¥ 〒 ¢ £ ※ ♀ ♂ ΑΒΓΔΕΖΗΘΙΚ∧ΜΝΞΟ∏Ρ∑ΤΥΦΧΨΩ α β γ δ ε ζ η θ ι κ λ μ ν ξ ο π ρ σ τ υ φ χ ψ ω АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ а б в г д е ё ж з и й к л м н о п р с т у ф х ц ч ш щ ъ ы ь э ю я ㄅㄉㄓㄚㄞㄢㄦㄆㄊㄍㄐㄔㄗㄧㄛㄟㄣㄇㄋㄎㄑㄕㄘㄨㄜㄠㄤㄈㄏㄒㄖㄙㄩㄝㄡㄥ ā á ǎ à、ō ó ǒ ò、ê ē é ě è、ī í ǐ ì、ū ú ǔ ù、ǖ ǘ ǚ ǜ ü ぁぃぅぇぉかきくけこんさしすせそたちつってとゐなにぬねのはひふへほゑまみむめもゃゅょゎを ァィゥヴェォカヵキクケヶコサシスセソタチツッテトヰンナニヌネノハヒフヘホヱマミムメモャュョヮヲ ˉˇ¨‘’々~‖∶”’‘|〃〔〕《》「」『』.〖〗【【】()〔〕{}[]~||¶µ©®ßΛΣΠ€♯♪♫ ⅠⅡⅢⅣⅤⅥⅦⅧⅨⅩⅪⅫ①②③④⑤⑥⑦⑧⑨⑩ ≈≡≠=≤≥ < > ≮≯∷±±×÷/∫∮∝∞∧∨∑∏∪∩∈∵∴⊥‖∠⌒⊙≌∽√ °′〃$£¥‰%℃¤¢ ┌┍┎┏┐┑┒┓—┄┈├┝┞┟┠┡┢┣|┆┊┬┭┮┯┰┱┲┳┼┽┾┿╀╂╁╃ §№☆★○●◎◇◆□■△▲※→←↑↓〓#&@^_ ▁▂▃▄▅▆▇█▉▊▋▌▍▎▏▓▔▕◢◣◤◥☉♀♂ ⊙●○①⊕◎Θ⊙¤㊣▂ ▃ ▄ ▅ ▆ ▇ █ █ ■ ▓ 回 □ 〓≡ ╝╚╔ ╗╬ ═ ╓ ╩ ┠ ┨┯ ┷┏ ┓┗ ┛┳⊥『』┌♀◆◇◣◢◥▲▼△▽⊿ abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 0123456789 ':;`:¡¢£¦«»­¯´·ˊˋƒ‒―‚„†‡•″‹›℅ℓΩ℮↔↕∂─│╒▀▐░▒▪▫◊◦♠♣♥♦⃝⃞‧℧∅∝∞()!*
7613个通用汉字 一丁七万丈三上下不与丐丑专且丕世丘丙业丛东丝丞丢两严丧个丫中丰串临丸丹为主丽举乂乃久么义之乌乍乎乏乐乒乓乔乖乘乙乜九乞也习乡书乩买乱乳乾了予争事二亍于亏云互亓五井亘亚些亟亡亢交亥亦产亨亩享京亭亮亲亳亵亶亸人亿什仁仂仃仄仅仆仇仉今介仍从仑仓仔仕他仗付仙仞仟仡代令以仨仪仫们仰仲仳仵件价任份仿企伉伊伋伍伎伏伐休众优伙会伛伞伟传伢伤伥伦伧伪伫伯估伴伶伸伺似伽伾佃但位低住佐佑体何佗佘余佚佛作佝佞佟你佣佤佥佩佬佯佰佳佴佶佻佼佾使侃侄侈侉例侍侏侑侔侗供依侠侣侥侦侧侨侩侪侬侮侯侵便促俄俅俊俎俏俐俑俗俘俚俜保俞俟信俣俦俨俩俪俭修俯俱俳俵俶俸俺俾倌倍倏倒倓倔倘候倚倜倞借倡倥倦倨倩倪倬倭债倻值倾偃假偈偌偎偏偕做停健偬偲偶偷偻偾偿傀傅傈傍傣傥傧储傩催傲傻僇像僖僚僦僧僬僭僮僰僳僵僻儆儇儋儒儡儿兀允元兄充兆先光克免兑兔兕兖党兜兢入全八公六兮兰共关兴兵其具典兹养兼兽冀冁内冈冉册再冒冕冗写军农冠冢冤冥冬冯冰冱冲决况冶冷冻冼冽净凄准凇凉凋凌减凑凛凝几凡凤凫凭凯凰凳凶凸凹出击凼函凿刀刁刃分切刈刊刍刎刑划刖列刘则刚创初删判刨利别刭刮到刳制刷券刹刺刻刽刿剀剁剂剃剅削剌前剐剑剔剕剖剜剞剟剡剥剧剩剪副割剽剿劁劂劈劐劓力劝办功加务劢劣动助努劫劬劭励劲劳劾势勃勇勉勋勍勐勒勖勘勚募勤勰勺勾勿匀包匆匈匍匏匐匕化北匙匜匝匠匡匣匦匪匮匹区医匾匿十千卅升午卉半华协卑卒卓单卖南博卜卞卟占卡卢卣卤卦卧卫卮卯印危即却卵卷卸卺卿厂厄厅历厉压厌厍厕厘厚厝原厢厣厥厦厨厩厮去厾县叁参又叉及友双反发叔取受变叙叛叟叠口古句另叨叩只叫召叭叮可台叱史右叵叶号司叹叻叼叽吁吃各吆合吉吊同名后吏吐向吓吕吗君吝吞吟吠吡吣否吧吨吩含听吭吮启吱吲吴吵吸吹吻吼吾呀呃呆呈告呋呐呓呔呕呖呗员呙呛呜呢呤呦周呱呲味呵呶呷呸呻呼命咀咂咄咆咋和咎咏咐咒咔咕咖咙咚咛咝咣咤咦咧咨咩咪咫咬咭咯咱咳咴咸咻咽咿哀品哂哄哆哇哈哉哌响哎哏哐哑哓哔哕哗哙哚哝哞哟哥哦哧哨哩哪哭哮哲哳哺哼哽哿唁唆唇唉唏唐唑唔唛唝唠唢唣唤唧唪唬售唯唱唳唷唼唾唿啁啃啄商啉啊啐啕啖啜啡啤啥啦啧啪啬啭啮啰啴啵啶啷啸啻啼啾喀喁喂喃善喇喈喉喊喋喏喑喔喘喙喜喝喟喤喧喱喳喵喷喹喻喽喾嗄嗅嗉嗌嗍嗑嗒嗓嗔嗖嗜嗝嗞嗟嗡嗣嗤嗥嗦嗨嗪嗫嗬嗯嗲嗳嗵嗷嗽嗾嘀嘁嘈嘉嘌嘎嘏嘘嘚嘛嘞嘟嘡嘣嘤嘧嘬嘭嘱嘲嘴嘶嘹嘻嘿噀噌噍噎噔噗噙噜噢噤器噩噪噫噬噱噶噻噼嚄嚅嚆嚎嚏嚓嚚嚣嚯嚷嚼囊囔囚四囝回囟因囡团囤囫园困囱围囵囹固国图囿圃圄圆圈圉圊圜土圣在圩圪圬圭圮圯地圳圹场圻圾址坂均坊坌坍坎坏坐坑块坚坛坜坝坞坟坠坡坤坦坨坩坪坫坭坯坳坷坻坼垂垃垄垅垆型垌垍垒垓垛垞垟垠垡垢垣垤垦垧垩垫垭垮垯垱垲垸埂埃埋城埏埒埔埕埘埙埚埝域埠埭埯埴埸培基埼埽堂堆堇堉堋堌堍堑堕堙堞堠堡堤堪堰堵塃塄塆塈塌塍塑塔塘塞塥填塬塮塾墀墁境墅墈墉墒墓墙增墟墦墨墩墼壁壅壑壕壤士壬壮声壳壶壹处备复夏夔夕外夙多夜够夤夥大天太夫夬夭央夯失头夷夸夹夺夼奁奂奄奇奈奉奋奎奏契奔奕奖套奘奚奠奢奥奭女奴奶奸她好妁如妃妄妆妇妈妊妍妒妓妖妗妙妞妣妤妥妨妩妪妫妮妯妲妹妻妾姆姊始姐姑姒姓委姗姘姚姜姝姞姣姥姨姬姹姻姽姿威娃娄娅娆娇娈娉娌娑娓娘娜娟娠娣娥娩娱娲娴娶娼婀婆婉婊婕婚婞婢婧婪婳婴婵婶婷婺婿媒媚媛媪媲媳媵媸媾嫁嫂嫉嫌嫒嫔嫖嫘嫚嫜嫠嫡嫣嫦嫩嫪嫫嫱嬉嬖嬗嬴嬷孀子孑孓孔孕字存孙孚孛孜孝孟孢季孤孥学孩孪孬孰孱孳孵孺孽宁它宄宅宇守安宋完宏宓宕宗官宙定宛宜宝实宠审客宣室宥宦宪宫宬宰害宴宵家宸容宽宾宿寂寄寅密寇富寐寒寓寝寞察寡寤寥寨寮寰寸对寺寻导寿封射将尉尊小少尔尕尖尘尚尜尝尤尥尧尬就尴尸尹尺尻尼尽尾尿局屁层屃居屈屉届屋屎屏屐屑展屙属屠屡屣履屦屯山屹屺屿岁岂岈岌岍岐岑岔岖岗岘岙岚岛岢岣岩岫岬岭岱岳岵岷岸岿峁峂峄峋峒峙峡峣峤峥峦峧峨峪峭峰峻崂崃崆崇崎崔崖崛崞崟崤崦崩崭崮崴崽嵇嵊嵋嵌嵎嵖嵘嵚嵛嵝嵩嵫嵬嵯嵴嶂嶓嶙嶝嶷巅巉巍川州巡巢工左巧巨巩巫差巯己已巳巴巷巽巾币市布帅帆师希帏帐帑帔帕帖帘帙帚帛帜帝帡带帧席帮帱帷常帻帼帽幂幄幅幌幔幕幛幞幡幢幪干平年并幸幺幻幼幽广庀庄庆庇床庋序庐庑库应底庖店庙庚府庞废庠庤庥度座庭庳庵庶康庸庹庼庾廉廊廋廒廓廖廙廛廨廪延廷建廿开弁异弃弄弇弈弊弋式弑弓引弗弘弛弟张弥弦弧弩弭弯弱弹强弼彀归当录彖彗彘彝形彤彦彧彩彪彬彭彰影彳彷役彻彼往征徂径待徇很徉徊律徐徒徕得徘徙徜御徨循徭微徵德徼徽心必忆忉忌忍忏忐忑忒忖志忘忙忝忠忡忤忧忪快忭忮忱念忸忻忽忾忿怀态怂怃怄怅怆怊怍怎怏怒怔怕怖怙怛怜思怠怡急怦性怨怩怪怫怯怵总怼怿恁恂恃恋恍恐恒恕恙恚恝恢恣恤恧恨恩恪恫恬恭息恰恳恶恸恹恺恻恼恽恿悃悄悉悌悍悒悔悖悚悛悝悟悠悢患悦您悫悬悭悯悱悲悴悸悻悼情惆惇惊惋惑惕惘惚惜惝惟惠惦惧惨惩惫惬惭惮惯惰想惴惶惹惺愀愁愆愈愉愎意愔愕愚感愠愣愤愦愧愫愿慈慊慌慎慑慕慝慢慥慧慨慭慰慵慷憋憎憔憝憧憨憩憬憷憾懂懈懊懋懑懒懦懵懿戆戈戊戋戌戍戎戏成我戒戕或戗战戚戛戟戡戢戥截戬戮戳戴户戽戾房所扁扃扅扇扈扉扊手才扎扑扒打扔托扛扣扦执扩扪扫扬扭扮扯扰扳扶批扺扼找承技抃抄抉把抑抒抓抔投抖抗折抚抛抟抠抡抢护报抨披抬抱抵抹抻押抽抿拂拃拄担拆拇拈拉拊拌拍拎拐拒拓拔拖拗拘拙拚招拜拟拢拣拤拥拦拧拨择括拭拮拯拱拳拴拶拷拼拽拾拿持挂指挈按挎挑挖挚挛挝挞挟挠挡挢挣挤挥挦挨挪挫振挲挹挺挽捂捃捅捆捉捋捌捍捎捏捐捕捞损捡换捣捧捩捭据捯捶捷捺捻掀掂掇授掉掊掌掎掏掐排掖掘掠探掣接控推掩措掬掭掮掰掳掴掷掸掺掼掾揄揆揉揍揎描提插揖揞揠握揣揩揪揭揳援揶揸揽揾揿搀搁搂搅搋搌搏搐搒搓搔搛搜搞搠搡搦搪搬搭搴携搽摁摄摅摆摇摈摊摒摔摘摞摧摩摭摸摹摺摽撂撄撅撇撑撒撕撖撙撞撤撩撬播撮撰撵撷撸撺撼擀擂擅操擎擐擒擘擞擢擤擦攀攉攒攘攥攫攮支收攸改攻放政故效敉敌敏救敕敖教敛敝敞敢散敦敫敬数敲整敷文斋斌斐斑斓斗料斛斜斝斟斡斤斥斧斩斫断斯新方於施旁旃旄旅旆旋旌旎族旒旖旗无既日旦旧旨早旬旭旮旯旰旱时旷旸旺旻昀昂昃昆昉昊昌明昏易昔昕昙昝星映春昧昨昭是昱昴昵昶昼昽显晁晃晋晌晏晒晓晔晕晖晗晚晞晟晡晢晤晦晨普景晰晴晶晷智晾暂暄暅暇暌暑暖暗暝暧暨暮暴暹暾曈曙曛曜曝曦曩曰曲曳更曷曹曼曾替最月有朊朋服朐朔朕朗朘望朝期朦木未末本札术朱朴朵机朽杀杂权杆杈杉杌李杏材村杓杖杜杞束杠条来杧杨杪杭杯杰杲杳杵杷杻杼松板极构枇枉枋析枕林枘枚果枝枞枢枣枥枧枨枪枫枭枯枰枳枵架枷枸柁柃柄柈柏某柑柒染柔柘柙柚柜柝柞柠柢查柩柬柯柰柱柳柴柽柿栀栅标栈栉栊栋栌栎栏树栒栓栖栗栝栟校栩株栲栳样核根格栽栾桀桁桂桃桄桅框案桉桊桌桎桐桑桓桔桕桡桢档桤桥桦桧桨桩桫桴桶桷梁梃梅梆梏梓梗梢梦梧梨梭梯械梳梵梽梾检棁棂棉棋棍棒棕棘棚棠棣棨棬森棰棱棵棹棺棻棼椁椅椋植椎椐椑椒椟椠椤椭椰椴椽椿楂楔楗楚楝楞楠楣楦楫楮楯楷楸楹楼榀概榄榆榇榈榉榍榔榕榖榛榜榧榨榫榭榴榷榻槁槊槌槎槐槔槚槛槜槟槠槭槲槽槿樊樗樘樟模樨横樯樱樵樽樾橄橇橐橘橙橛橡橥橦橱橹橼檀檄檎檐檑檗檠檩檫檬檵欠次欢欣欤欧欲欸欺款歃歆歇歉歌歙止正此步武歧歪歹死歼殁殂殃殄殆殇殉殊残殍殒殓殖殚殛殡殣殪殳殴段殷殿毁毂毅毋母每毐毒毓比毕毖毗毙毛毡毪毫毯毳毵毹毽氅氆氇氍氏氐民氓气氕氖氘氙氚氛氟氡氢氤氦氧氨氩氪氮氯氰氲水永氽汀汁求汆汇汈汉汊汐汔汕汗汛汜汝汞江池污汤汨汩汪汭汰汲汴汶汹汽汾沁沂沃沄沅沆沈沉沌沏沐沓沔沘沙沚沛沟没沣沤沥沦沧沨沩沪沫沭沮沱河沸油治沼沽沾沿泃泄泅泉泊泌泐泓泔法泖泗泚泛泜泞泠泡波泣泥注泪泫泮泯泰泱泳泵泷泸泺泻泼泽泾洁洄洇洋洌洎洑洒洗洙洚洛洞洣津洧洨洪洫洮洱洲洳洴洵洹洺活洼洽派流浃浅浆浇浈浉浊测浍济浏浐浑浒浓浔浕浙浚浜浞浠浡浣浥浦浩浪浮浯浴海浸浼涂涅消涉涌涎涑涓涔涕涘涛涝涞涟涠涡涢涣涤润涧涨涩涪涫涮涯液涵涸涿淀淄淅淆淇淋淌淏淑淖淘淙淝淞淠淡淤淦淫淬淮深淳混淹添清渊渌渍渎渐渑渔渗渚渝渠渡渣渤渥温渫渭港渲渴游渺湃湄湉湍湎湔湖湘湛湜湝湟湨湫湮湲湾湿溃溅溆溇溉溏源溘溜溟溠溢溥溦溧溪溯溱溲溴溶溷溺溻溽滁滂滃滇滋滍滏滑滓滔滕滗滘滚滞滟滠满滢滤滥滦滨滩滪滫滴滹漂漆漉漏漓演漕漠漤漩漪漫漭漯漱漳漶漾潆潇潋潍潏潘潜潞潟潢潦潭潮潲潴潵潸潺潼潽澄澈澉澌澍澎澜澡澥澧澳澴澶澹澼激濂濉濑濒濞濠濡濮濯瀌瀍瀑瀚瀛瀣瀵瀹灌灏灞火灭灯灰灵灶灸灼灾灿炀炅炉炊炎炒炔炕炖炙炜炝炟炫炬炭炮炯炱炳炷炸点炻炼炽烀烁烂烃烈烊烘烙烛烜烝烟烤烦烧烨烩烫烬热烯烷烹烺烽焉焊焌焐焓焕焖焘焙焚焜焦焯焰焱然煅煊煌煎煜煞煤煦照煨煮煲煳煸煺煽熄熊熏熔熘熙熜熟熠熥熨熬熵熹燃燊燎燏燔燕燠燥燧燮燹爆爝爨爪爬爰爱爵父爷爸爹爻爽爿牁牂片版牌牍牒牖牙牚牛牝牟牡牢牦牧物牮牯牲牵特牺牾犀犁犄犊犋犍犏犒犟犨犬犯犰犴状犷犸犹狁狂狃狄狈狉狍狎狐狒狗狙狝狞狠狡狨狩独狭狮狯狰狱狲狳狴狷狸狺狻狼猁猃猄猊猎猕猖猗猛猜猝猞猡猢猥猩猪猫猬献猱猴猷猸猹猾猿獍獐獒獗獠獬獭獯獴獾玄率玉王玎玑玕玖玙玚玛玠玡玢玥玦玩玫玮环现玲玳玷玺玻珀珂珈珉珊珍珏珐珑珙珞珠珣珥珧珩班珰珲珽球琅理琇琉琎琏琐琚琛琢琤琥琦琨琪琫琬琮琯琰琳琴琵琶琼瑀瑁瑄瑕瑗瑙瑚瑛瑜瑞瑟瑢瑭瑰瑶瑾璀璁璃璆璇璈璋璎璐璘璜璞璟璠璧璨璩璪瓒瓘瓜瓞瓠瓢瓣瓤瓦瓮瓯瓴瓶瓷瓻瓿甄甍甏甑甓甘甙甚甜生甥用甩甪甫甬甭田由甲申电男甸町画甾畀畅畈畋界畎畏畔留畚畛畜略畦番畯畲畴畸畹畿疃疆疍疏疑疔疖疗疙疚疝疟疠疡疢疣疤疥疫疬疭疮疯疰疱疲疳疴疵疸疹疼疽疾痂痃痄病症痈痉痊痍痒痔痕痘痛痞痢痣痤痦痧痨痪痫痰痱痴痹痼痿瘁瘃瘅瘆瘊瘌瘐瘗瘘瘙瘛瘟瘠瘢瘤瘥瘦瘩瘪瘫瘭瘰瘳瘴瘵瘸瘼瘾瘿癀癃癌癍癔癖癜癞癣癫癯癸登白百皂的皆皇皈皋皎皑皓皖皤皮皱皲皴皿盂盅盆盈盉益盍盎盏盐监盒盔盖盗盘盛盟盥盦目盯盱盲直相盹盼盾省眄眇眈眉眊看眍眙眚真眠眢眦眨眩眬眭眯眵眶眷眸眺眼着睁睃睇睐睑睚睛睡睢督睥睦睨睫睬睹睽睾睿瞀瞄瞅瞌瞍瞎瞑瞒瞟瞠瞢瞥瞧瞩瞪瞬瞭瞰瞳瞵瞻瞽瞿矍矗矛矜矢矣知矧矩矫矬短矮石矶矸矻矽矾矿砀码砂砉砌砍砑砒研砖砗砘砚砜砝砟砣砥砧砭砮砰破砷砸砹砺砻砼砾础硁硅硇硌硎硐硒硕硖硗硚硝硪硫硬硭确硷硼碇碉碌碍碎碑碓碗碘碚碛碜碟碡碣碥碧碰碱碲碳碴碶碹碾磁磅磉磊磋磐磔磕磙磨磬磲磴磷磺礁礅礌礓礞礤礳礴示礼社祀祁祃祆祈祉祎祓祖祗祚祛祜祝神祟祠祢祥祧票祭祯祲祷祸祺祼祾禀禁禄禅禊福禚禤禧禳禹禺离禽禾秀私秃秆秉秋种科秒秕秘租秣秤秦秧秩秫秭积称秸移秽秾稀稂稃稆程稍税稔稗稚稞稠稣稳稷稻稼稽稿穄穆穑穗穰穴究穷穸穹空穿窀突窃窄窅窈窍窑窒窕窖窗窘窜窝窟窠窣窥窦窨窬窭窳窸窿立竑竖站竞竟章竣童竦竭端竹竺竽竿笃笄笆笈笊笋笏笑笔笕笙笛笞笠笤笥符笨笪笫第笮笱笳笸笺笼笾筅筇等筋筌筏筐筑筒答策筘筚筛筜筝筠筢筮筱筲筵筷筹筻筼签简箅箍箐箓箔箕算箜管箢箦箧箨箩箪箫箬箭箱箴箸篁篆篇篌篑篓篙篚篝篡篥篦篪篮篱篷篼篾簃簇簉簋簌簏簖簟簠簦簧簪簸簿籀籁籍米籴类籼籽粉粑粒粕粗粘粜粝粞粟粤粥粪粮粱粲粳粹粼粽精糁糅糇糈糊糌糍糕糖糗糙糜糟糠糨糯糵系紊素索紧紫累絮絷綦綮縻繁繄繇纂纛纠纡红纣纤纥约级纨纩纪纫纬纭纯纰纱纲纳纴纵纶纷纸纹纺纻纽纾线绀绁绂练组绅细织终绉绊绋绌绍绎经绑绒结绔绕绗绘给绚绛络绝绞统绠绡绢绣绤绥绦继绨绩绪绫续绮绯绰绲绳维绵绶绷绸绹绺绻综绽绾绿缀缁缂缃缄缅缆缇缈缉缌缎缏缑缒缓缔缕编缗缘缙缚缛缜缝缟缠缡缢缣缤缥缦缧缨缩缪缫缬缭缮缯缰缱缲缳缴缵缶缸缺罂罄罅罐网罔罕罗罘罚罟罡罢罨罩罪置罱署罴罹罽罾羁羊羌美羑羔羚羝羞羟羡群羧羯羰羲羸羹羼羽羿翁翅翊翌翎翔翕翘翙翚翟翠翡翥翦翩翮翯翰翱翳翻翼耀老考耄者耆耋而耍耐耒耔耕耖耗耘耙耜耠耢耥耦耧耨耩耪耰耱耲耳耵耶耷耸耻耽耿聂聃聆聊聋职聍聒联聘聚聩聪聱聿肃肄肆肇肉肋肌肓肖肘肚肛肝肟肠股肢肤肥肩肪肫肭肮肯肱育肴肷肺肼肽肾肿胀胁胂胃胄胆背胍胎胖胗胙胚胛胜胝胞胡胤胥胧胨胩胪胫胬胭胯胰胱胲胳胴胶胸胺胼能脂脆脉脊脍脎脏脐脑脒脓脔脖脘脚脞脬脯脱脲脶脸脾腆腈腊腋腌腐腑腒腓腔腕腙腚腠腥腧腩腭腮腰腱腴腹腺腻腼腽腾腿膀膂膈膊膏膑膘膙膛膜膝膦膨膪膳膺膻臀臁臂臃臆臊臌臑臜臣臧自臬臭至致臻臼臾舀舂舄舅舆舌舍舐舒舔舛舜舞舟舢舣舨航舫般舰舱舳舴舵舶舷舸船舻舾艄艇艋艘艚艟艨艮良艰色艳艴艺艽艾艿节芄芈芊芋芍芎芏芑芒芗芙芜芝芟芡芥芦芨芩芪芫芬芭芮芯芰花芳芴芷芸芹芼芽芾苁苄苇苈苊苋苌苍苎苏苑苒苓苔苕苗苘苛苜苞苟苠苡苣苤若苦苫苯英苴苷苹苻茀茁茂范茄茅茆茈茉茌茎茏茑茓茔茕茗茚茛茜茝茧茨茫茬茭茯茱茳茴茵茶茸茹茼荀荃荆荇草荏荐荑荒荔荙荚荛荜荞荟荠荡荣荤荥荦荧荨荩荪荫荬荭药荷荸荻荼荽莅莆莉莎莒莓莘莙莛莜莞莠莨莩莪莫莰莱莲莳莴莶获莸莹莺莼莽菀菁菂菅菇菊菌菏菔菖菘菜菝菟菠菡菥菩菪菰菱菲菹菼菽萁萃萄萋萌萍萎萏萑萘萜萝萤营萦萧萨萩萱萸萼落葆葑葓葖著葚葛葜葡董葩葫葬葭葱葳葵葶葸葺蒂蒇蒈蒉蒋蒌蒎蒗蒙蒜蒟蒡蒯蒲蒴蒸蒹蒺蒽蒿蓁蓂蓄蓇蓉蓊蓍蓐蓑蓓蓖蓝蓟蓠蓣蓥蓦蓬蓰蓼蓿蔌蔑蔓蔗蔚蔟蔡蔫蔬蔷蔸蔹蔺蔻蔼蔽蕃蕈蕉蕊蕖蕙蕞蕤蕨蕰蕲蕴蕹蕺蕻蕾薄薅薇薏薛薜薤薨薪薮薯薰薷薹藁藉藏藐藓藕藜藠藤藩藻藿蘅蘑蘖蘘蘧蘩蘸蘼虎虏虐虑虔虚虞虢虫虬虮虱虹虺虻虼虽虾虿蚀蚁蚂蚊蚋蚌蚍蚓蚕蚜蚝蚣蚤蚧蚨蚩蚪蚬蚯蚰蚱蚴蚶蚺蛀蛄蛆蛇蛉蛊蛋蛎蛏蛐蛑蛔蛘蛙蛛蛞蛟蛤蛩蛭蛮蛰蛱蛲蛳蛴蛸蛹蛾蜀蜂蜃蜇蜈蜉蜊蜍蜎蜒蜓蜕蜗蜘蜚蜜蜞蜡蜢蜣蜥蜩蜮蜱蜴蜷蜻蜾蜿蝇蝈蝉蝌蝎蝓蝗蝙蝠蝣蝤蝥蝮蝰蝴蝶蝻蝼蝽蝾螂螃螅螈螋融螗螟螠螣螨螫螬螭螯螳螵螺螽蟀蟆蟊蟋蟑蟒蟛蟠蟥蟪蟮蟹蟾蠃蠊蠋蠓蠕蠖蠡蠢蠲蠹蠼血衄衅行衍衔街衙衡衢衣补表衩衫衬衮衰衲衷衽衾衿袁袂袄袅袆袈袋袍袒袖袗袜袢袤袪被袭袯袱袼裁裂装裆裈裉裎裒裔裕裘裙裟裢裣裤裥裨裰裱裳裴裸裹裼裾褂褊褐褒褓褙褚褛褡褥褪褫褰褴褶襁襄襕襞襟襦襻西要覃覆见观规觅视觇览觉觊觋觌觎觏觐觑角觖觚觜觞解觥触觫觯觱觳言訄訇訾詈詹誉誊誓謇警譬计订讣认讥讦讧讨让讪讫训议讯记讲讳讴讵讶讷许讹论讼讽设访诀证诂诃评诅识诈诉诊诋诌词诎诏诐译诒诓诔试诖诗诘诙诚诛诜话诞诟诠诡询诣诤该详诧诨诩诫诬语诮误诰诱诲诳说诵请诸诹诺读诼诽课诿谀谁谂调谄谅谆谇谈谊谋谌谍谎谏谐谑谒谓谔谕谖谗谙谚谛谜谝谟谠谡谢谣谤谥谦谧谨谩谪谫谬谭谮谯谰谱谲谳谴谵谶谷豁豆豇豉豌豕豚象豢豨豪豫豳豸豹豺貂貅貉貊貌貔貘贝贞负贡财责贤败账货质贩贪贫贬购贮贯贰贱贲贳贴贵贶贷贸费贺贻贼贽贾贿赁赂赃资赅赆赇赈赉赊赋赌赍赎赏赐赑赓赔赖赘赙赚赛赜赝赞赟赠赡赢赣赤赦赧赪赫赭走赳赴赵赶起趁趄超越趋趑趔趟趣趱足趴趵趸趺趼趾趿跃跄跆跋跌跎跏跐跑跖跗跚跛距跞跟跣跤跨跪跬路跳践跶跷跸跹跺跻跽踅踉踊踌踏踔踝踞踟踢踣踩踪踬踮踯踱踵踶踹踺踽蹀蹁蹂蹄蹅蹇蹈蹉蹊蹋蹐蹑蹒蹓蹙蹜蹢蹦蹩蹬蹭蹯蹰蹲蹴蹶蹼蹽蹾蹿躁躅躇躏躐躔躜躞身躬躯躲躺车轧轨轩轪轫转轭轮软轰轱轲轳轴轵轶轷轸轹轺轻轼载轾轿辀辁辂较辄辅辆辇辈辉辊辋辌辍辎辏辐辑辒输辔辕辖辗辘辙辚辛辜辞辟辣辨辩辫辰辱边辽达迁迂迄迅过迈迎运近迓返迕还这进远违连迟迢迤迥迦迨迩迪迫迭迮述迷迸迹追退送适逃逄逅逆选逊逋逍透逐逑递途逖逗通逛逝逞速造逡逢逦逭逮逯逵逶逸逻逼逾遁遂遄遇遍遏遐遑遒道遗遘遛遢遣遥遨遭遮遴遵遽避邀邂邃邈邋邑邓邕邗邙邛邝邡邢那邦邪邬邮邯邰邱邳邴邵邶邸邹邺邻邽邾郁郄郅郇郈郊郎郏郐郑郓郗郚郛郜郝郡郢郤郦郧部郫郭郯郴郸都郾郿鄂鄄鄌鄘鄙鄞鄢鄣鄯鄱鄹酃酆酉酊酋酌配酎酏酐酒酗酚酝酞酡酢酣酤酥酦酩酪酬酮酯酰酱酲酴酵酶酷酸酹酽酾酿醅醇醉醋醌醍醐醑醒醚醛醢醪醭醮醯醴醵醺醾采釉释里重野量金釜鉴銎銮鋆鋈錾鍪鎏鏊鏖鐾鑫钆钇针钉钊钋钌钍钎钏钐钒钓钔钕钗钘钙钚钛钝钞钟钠钡钢钣钤钥钦钧钨钩钪钫钬钭钮钯钰钱钲钳钴钵钷钹钺钻钼钽钾钿铀铁铂铃铄铅铆铈铉铊铋铌铍铎铐铑铒铕铗铘铙铚铛铜铝铞铟铠铡铢铣铤铥铧铨铩铪铫铬铭铮铯铰铱铲铳铴铵银铷铸铹铺铻铼铽链铿销锁锂锃锄锅锆锇锈锉锊锋锌锎锏锐锑锒锓锔锕锖锗锘错锚锛锜锝锞锟锡锢锣锤锥锦锧锨锩锪锫锬锭键锯锰锱锲锴锵锶锷锸锹锻锽锾锿镀镁镂镃镄镅镆镇镈镉镊镋镌镍镎镏镐镑镒镓镔镖镗镘镚镛镜镝镞镠镡镢镣镤镥镦镧镨镩镪镫镬镭镯镰镱镲镳镴镵镶长门闩闪闭问闯闰闱闲闳间闵闶闷闸闹闺闻闼闽闾闿阀阁阂阃阄阅阆阇阈阉阊阋阌阍阎阏阐阑阒阔阕阖阗阘阙阚阜队阡阢阪阮阱防阳阴阵阶阻阼阽阿陀陂附际陆陇陈陉陋陌降限陔陕陛陟陡院除陧陨险陪陬陲陴陵陶陷隅隆隈隋隍随隐隔隗隘隙障隧隰隳隶隼隽难雀雁雄雅集雇雉雌雍雎雏雒雕雠雨雩雪雯雳零雷雹雾需霁霄霆震霈霉霍霎霏霓霖霜霞霪霭霰露霸霹霾青靓靖静靛非靠靡面靥革靰靳靴靶靸靺靼靽靿鞁鞅鞋鞍鞑鞒鞘鞠鞡鞣鞧鞨鞫鞭鞯鞲鞴韂韦韧韨韩韪韫韬韭音韵韶页顶顷顸项顺须顼顽顾顿颀颁颂颃预颅领颇颈颉颊颋颌颍颏颐频颓颔颖颗题颙颚颛颜额颞颟颠颡颢颤颥颦颧风飐飑飒飓飔飕飗飘飙飞食飧飨餍餐餮饔饕饥饧饨饩饪饫饬饭饮饯饰饱饲饳饴饵饶饷饸饹饺饻饼饽饿馀馁馃馄馅馆馇馈馉馊馋馌馍馏馐馑馒馓馔馕首馗馘香馥馨马驭驮驯驰驱驳驴驵驶驷驸驹驺驻驼驽驾驿骀骁骂骄骅骆骇骈骊骋验骍骎骏骐骑骒骓骖骗骘骙骚骛骜骝骞骟骠骡骢骣骤骥骧骨骰骶骷骸骺骼髀髁髂髅髋髌髑髓高髡髦髫髭髯髹髻鬃鬈鬏鬓鬟鬣鬯鬲鬶鬻鬼魁魂魃魄魅魆魇魈魉魍魏魑魔鱼鱽鱾鱿鲀鲁鲂鲃鲅鲆鲇鲈鲉鲊鲋鲌鲍鲎鲏鲐鲑鲔鲙鲚鲛鲜鲞鲟鲠鲡鲢鲣鲤鲥鲦鲧鲨鲩鲪鲫鲬鲭鲮鲯鲰鲱鲲鲳鲴鲵鲷鲸鲹鲺鲻鲼鲽鲾鳀鳁鳂鳃鳄鳅鳆鳇鳈鳉鳊鳌鳍鳎鳏鳐鳑鳓鳔鳕鳖鳗鳘鳙鳚鳜鳝鳞鳟鳡鳢鳤鸟鸠鸡鸢鸣鸤鸥鸦鸨鸩鸪鸫鸬鸭鸮鸯鸰鸱鸲鸳鸵鸶鸷鸸鸹鸺鸻鸼鸽鸾鸿鹀鹁鹂鹃鹄鹅鹆鹇鹈鹉鹊鹋鹌鹎鹏鹐鹑鹕鹗鹘鹚鹛鹜鹝鹞鹟鹠鹡鹣鹤鹦鹧鹨鹩鹪鹫鹬鹭鹮鹰鹱鹲鹳鹿麂麇麈麋麒麓麝麟麦麸麻麽麾黄黇黉黍黎黏黑黔默黛黜黝黟黠黢黥黧黩黯黹黻黼黾鼋鼍鼎鼐鼒鼓鼗鼙鼠鼢鼩鼫鼬鼯鼱鼷鼹鼻鼾齁齉齐齑齿龀龁龃龄龅龆龇龈龉龊龋龌龙龚龛龟龠仾伀伂伃伄伅伆伇伈伌伒伓伔伕伖伜伝伡伣伨伩伬伭伮伱伲伳伵伷伹伻伿佀佁佂佄佅佇佈佉佊佋佌佒佔佖佡佢咑咓咗咘咜咞咟咠咡咢咥咮咰咲咵咶咷咹咺咼咾哃哅哊哋哒哖哘哛哜哠員哢哣哤哫哬哯哰哱哴哵哶哷哸哹帗帞帟帠帢帣帤帥帨帩帪師帬帯帰帲帳帴帵帶帹帺帾帿幀幁幃幆幇幈幉幊幋幍幎幏幐幑幒幓幖幗幘幙幚幜幝幟幠幣幤幥幦幧幨幩幫幬幭幮幯幰幱幵幷幹幾庁庂広庅庈庉庌庍庎庒庘庛庝庡庢庣庨庩庪庫庬庮庯庰庱庲庴庺庻庽庿廀廁廂廃廄廅廆廇廈廌廍廎廏廐廑廔廕廗廘廚廜廝廞廟廠廡廢廣廤廥廦廧廩廫廬廭廮廯廰廱廲廳廴廵廸廹廻廼廽廾弅弆弉弌弍弎弐弒虲虳虴虵虶虷虸蚃蚄蚅蚆蚇蚈蚉蚎蚏蚐蚑蚒蚔蚖蚗蚘蚙蚚蚛蚞蚟蚠蚡蚢蚥蚦蚫蚭蚮蚲蚳蚵蚷蚸蚹蚻蚼蚽蚾蚿蛁蛂蛃蛅蛈蛌蛍蛒蛓蛕蛖蛗蛚蛜蛝蛠蛡蛢蛣蛥蛦蛧蛨蛪蛫蛬蛯蛵蛶蛷蛺蛻蛼蛽蛿蜁蜄蜅蜆蜋蜌蜏蜐蜑蜔蜖蜙蜛蜝蜟蜠蜤蜦蜧蜨蜪蜫蜬蜭蜯蜰蜲蜳蜵蜶蜸蜹蜺蜼蜽蝀蝁蝂蝃蝄蝅蝆蝊蝋蝍蝏蝐蝑蝒蝔蝕蝖蝘蝚蝛蝜蝝蝞蝟蝡蝢蝦蝧蝨蝩蝪蝫蝬蝭蝯蝱蝲蝳蝵蝷蝸蝹蝺蝿螀螁螄螆螇螉螊螌螎螏螐螑螒螓螔螕螖螘螙螚螛螜螝螞螡螢螤螥螦螧螩螪螮螰螱螲螴螶螷螸螹螻螼螾螿蟁蟂蟃蟄蟅蟇蟈蟉蟌蟍蟎蟏蟐蟓蟔蟕蟖蟗蟘蟙蟚蟜蟝蟞蟟蟡蟢蟣蟤蟦蟧蟨蟩蟫蟬蟭蟯蟰蟱蟲蟳蟴蟵蟶蟷蟸蟺蟻蟼蟽蟿蠀蠁蠂蠄蠅蠆蠇蠈蠉蠌蠍蠎蠏蠐蠑蠒蠔蠗蠘蠙蠚蠛蠜蠝蠞蠟蠠蠣蠤蠥蠦蠧蠨蠩蠪蠫蠬蠭蠮蠯蠰蠱蠳蠴蠵蠶蠷蠸蠺蠻蠽蠾蠿衁衂衃衆衇衈衉衊衋衎衏衐衑衒術衕衖衘衚衛衜衝衞衟衠衤衦衧衪衭衯衱衳衴衵衶衸衹衺衻衼袀袃袇袉袊袌袎袏袐袑袓袔



多媒体
2020-03-25 17:48:00
「深度学习福利」大神带你进阶工程师,立即查看>>> 腾讯会议去年推出,疫情期间两个月急速扩容,日活跃账户数已超过1000万,成为了当前中国最多人使用的视频会议应用。腾讯会议突围背后,是如何通过端到端实时语音技术保障交流通畅的?本文是腾讯多媒体实验室音频技术中心高级总监商世东老师在「云加社区沙龙online」的分享整理,从实时语音通信的发展历程,到5G下语音通信体验的未来,为你一一揭晓。
点击此链接,查看完整直播视频回放 ​
一、通信系统的衍变
1. 从模拟电话到数字电话
说到腾讯会议背后的实时语音端到端解决方案,大家可能第一时间就想到了PSTN电话,从贝尔实验室创造模拟电话开始,经过一百多年的发展,整个语音通信、语音电话系统经历了很大一部分变化。尤其是最近三十年来,语音通话由模拟信号变为数字信号,从固定电话变为移动电话,从电路交换到现在的分组交换。
以前的PSTN电话系统,用的都是老式模拟话机。然后数字相对模拟电话的优势是显而易见的,尤其在通话语音质量上抗干扰,抗长距离信号衰减的能力明显优于模拟电话和系统,所以电话系统演进的第一步就是从终端从模拟电话升级到了数字电话,网络也升级到了ISDN(综合业务数字网),可以支持数字语音和数据业务。
ISDN的最重要特征是能够支持端到端的数字连接,并且可实现话音业务和数据业务的综合,使数据和话音能够在同一网络中传递。但是本质上,ISDN还是电路交换网络系统。
所谓的电路交换,就是两个电话之间有一条专有的电路连接。基于专有电路连接的好处就是通话质量稳定。保证了链路的稳定性和通信的质量,同时也保证了整个通信的私密性。但是,这种基于电路交换的PSTN电话系统带来的弊端也很明显,尤其是打长途电话的时候。长途电话是基于专有线路,所以价格会非常昂贵。
同时,这一阶段,基于IP的互联网开始蓬勃发展,已通话为目的的通信终端也开始了从电路交换到分组交换的演进。如上图所示,分组交换的好处就是:可以分享带宽,整个链路连接并不是通话双方专享,而是很多电话共享的。共享带来的好处就是成本大幅度下降,同时,也进一步推动了整个电话语音通信技术的不断发展。
2. 从数字电话到IP电话
从2000年左右,当网络开始经历开始从电路交换到IP分组交换这样的衍进过程当中,近十年大家又开始面临一个新的挑战:整个网络、通信的终端较以前变得纷繁复杂,更加多样化。
以前主要就是电话与电话之间的通话,现在大家可以使用各种基于IP网络的客户端,比如PC、移动App,电话等通话,电话到电话间可以通过传统的电路交换,也可以是基于IP网络的数字电话。这样就导致了一个很显著的问题:整个网络开始变得异常复杂,异常多样化,终端也变成异样多样化。
在这样一个衍进过程当中,如何保证它们之间的互通性?传统的电话终端,跟不同互联网电话终端之间怎样解决互联互通的问题,又如何保证通话的质量和通话的体验呢?
3. H323与SIP协议
对于语音通话,不管是基于VoIP技术,还是基于传统的电路交换的电话,都有两个问题需要解决:首先需要注册到电话网里去,注册进去以后,在拨打电话的过程中,还需要弄清以下这些问题:怎样建立一个电话、怎样维护这个电话,以及最后怎样关闭这个电话?
电话建立起来以后还要进行能力协商,如果是IP电话,能力协商的本质就是双方交换彼此的IP和端口地址,建立逻辑通道才能进行通话。
在PSTN电话网络向IP电话网络衍进的过程当中,出现了两个非常有意思的协议族,第一个是H323协议。这个协议来自国际电信联盟ITO,它是传统制定电报电信标准的国际化组织。还有一个协议来自于互联网IETF(互联网工作组)制定的有关Internet各方面的很多标准。这两个标准协议的国际化组织各自推出相应的面对互联网通话的一整套解决方案。
H323协议族解决方案贯彻了ITO组织一贯的严谨,大包大揽的作风,整个协议族定义的非常完整和详细。从应用层到下面的传输层,使用H.225协议注册电话,用H225.0协议建立和维护电话,以及用H245在整个电话过程当中进行各种能力协商,进行IP地址的交换......这样一整套协议的制定,包括下面传输音视频使用RTP协议进行码流的传输,用RTCP协议进行整个码流的带宽控制,统计信息的上报,以及整个RTP协议上的音视频编码格式设置。整个H323协议族定义得非常详细而又完整,可以用做互联网上进行音视频通话的标准。
这个标准被很多大公司采用,像思科和微软的产品都遵循过H323标准。但是即使H323标准定义得如此完整和详细,它的市场推进速度却依然很慢。
而SIP协议来自IETF互联网工作组,互联网工作组的风格是开放和灵活的,所以他的整个协议也完全继承了其一贯的开放与灵活的思路。整体架构非常简单,SIP协议相对于H.323来说,并不规定媒体流具体是什么,只规定信令。整个SIP是利用互联网上已有的被广泛采用的像HTPP协议进行传输,整个message包全部都是用文本格式写的。所以在它各个不同的Entity之间,包括电话、Prosy、DNS、Location servier之间的通信是开放而又灵活的。
它不规定具体内容,只规定整个SIP协议有什么框架,什么样的网络结构,SIP模块之间互相通信遵循什么协议,例如用SDP协议来进行通信。通信格式也不是二进制,而H323协议就是二进制格式,非常难以扩展和阅读。
SIP协议非常开放和灵活,于是被很多公司和产品广泛采用,用在互联网通话过程中的通话建立,通话维护。但是它也有自身的弊端,那就是各个厂商之间的SIP解决方案往往难以互联互通。
H232和SIP协议,由于它们之间的定位不同,两家国际标准化组织的风格不同,在市场上也没有绝对的一家独大,各自都保留相应的市场份额。也正是因为有了H323或者SIP协议的出现,才使互联网上基于IP音视频的通话有了可能。
腾讯会议系统里面的音频解决方案正是这两个协议族和框架,在整个信令的解决方案上采用了H323协议,跟PSTN电话进行互联互通。在互联网和VoIP客户端之间采用SIP协议进行互联互通。
4. VoIP技术面临的困难和挑战
VoIP技术是基于当前这样复杂IP网络环境中,同样面临很多挑战。在电路交换中,因为资源是独占的,虽然贵但是质量可以得到保证。但是基于VoIP的解决方案是分组网络,不是独占资源,就会面临很多网络架构上的挑战,以及来自声学方面的挑战。
(1)丢包挑战
网络架构上的第一挑战是丢包,因为不是独有,而且整个UDP协议也不保证整个包一定送达目的地。
(2)延时挑战
第二个挑战是延时。整个IP网络存在很多交换机、存在各种中间交换节点,在各交换节点会产生延时。
(3)Jitter
第三个挑战是分组交换独有的一个概念:Jitter。就是对于延时的变化,虽然从发送的时间上来看,第一个包发出的时间比第二个包要早,但是到达目的地却可能是第二个包先到,导致就算收到第二个包,但是没有收到第一个包,语音也不能放出来。
(4)回声问题
VoIP电话相对于PSTN电话,会面临延时带来的挑战,导致我们在Echo的处理上也和传统大为不同。
传统电话很多时候不用考虑Echo,因为本地电话基本延时都能控制在50毫秒以内,人眼是分辨不出来到底是回声还是自己的讲话声音。但是互联网上因为Delay增大,甚至可能超过150毫秒,所以必须要把回声问题很好地解决,否则人耳听起来会感觉非常不舒服。
(5)带宽问题
另外整个网络的带宽,也跟通话质量息息相关。如果容量不够,对于VoIP通话路数和质量也会有很大的影响。
5. 腾讯会议的音视频解决方案
下图所示的是VoIP协议栈里面的一个主要框架,H323协议、SIP协议,它们各自在整个OSI集成网络模型中对应什么样的Layer,不同Layer之间是怎样进行交互的。
在整个腾讯会议语音通信里,H323和SIP信令怎样才能把呼叫建立起来,建立起来以后最重要的音视频媒体流在网上又是怎么传输的呢?
(1)实时语音通信:RTP协议
业界对于实时语音通信普遍采用的是RTP协议,RTP协议是基于UDP协议。因为它是UDP协议,所以跟TCP不太一样,它并不能保证无丢包,它是只要有包就想尽办法传送目的地。
RTP在语音通讯的过程中肯定不能直接跑在UDP上,因为语音通话对于丢包,抖动导致的语音卡顿非常敏感,但是也不能采用TCP协议,因为带来的延时太大。
所以目前大家都会采用RTP协议。RTP协议有一些机制,有两个典型的字段:Sequence Number 和 Time Stamp。通过这两个字段保证到达接收端的语音包在不连续或者乱序的情况下依然能通过一定的机制解决这个问题,在抖动不过大、丢包不过大的情况下不至于使语音通信的质量过低。
同时RTP协议里面,对于电话系统来说,语音通话存在多路流的情况。多人讲话,有音频、有视频,所以RTP定义了SRSC Identifier,不同的SRSC对应不同的音频流,不管是客户端还是服务器都可以根据情况进行混音或者混流的操作。
(2)Opus语音引擎
基于互联网的VoIP解决方案其实有很多选择,从最早的H323、G.711系列开始前前后后二三十年有几十种标准出现,但是目前Opus大有一统江湖的趋势。
从下图可以看出,整个Opus 覆盖了很宽的bite rate,从几kbps到几十kbps,Opus不光支持语音,也可以很好的支持到音乐场景,将来腾讯会议业务范围在音乐场景上也会占有一定的比例。
同时Opus还是一个低延时的语音引擎,因为在实时语音通讯中延时显得相当重要,延时超过200毫秒对于实时语音通信来说是显然不行的。
二、腾讯会议用户痛点和技术难点
在真正使用技术解决腾讯会议当中的音频问题的时候,还是能碰到很多的难点和痛点。我们在腾讯会议开发过程当中发现,用户在实际的使用体验过程中,由于各种各样的原因,导致出现许多问题。
1. 常见声音问题
(1)无声问题
首先是无声问题,例如通过VoIP客户端或者通过电话入会过程当中就能碰到无声问题,像驱动异常,硬件设备异常,无麦克风权限,设备初始化,电话打断等也能造成无声问题。
(2)漏回声
在实时语音过程当中还会出现漏回声的问题,在传统的PSTN电话系统中基本不存在回声,因为延时比较低,而且大部分电话都是话筒模式,很少使用外放。但是使用VoIP客户端,比如说PC和手机终端,越来越多的人喜欢使用外放,而不需要把耳机放在耳朵,这样就容易产生回声问题。
(3)声音嘈杂
同样还有声音嘈杂的问题,比如在移动场景,室外,或者是办公室里办公,大家使用VoIP客户端会经常听到办公室里的敲键盘声音、水杯喝水的声音,这些嘈杂声在以前使用普通电话话筒模式下并不明显。
(4)音量小,飘忽不定
还会有音量小,音量飘忽不定的情况出现,这也是跟使用的外设和使用的场景相关。像基于PC、Mac或者移动设备的系统播放回调过高,系统CPU载入过高,手持麦克风姿势不对,音乐语音判断错误,还有网络Jitter导致加减速,这些情况都会导致会议语音过程中碰到各种各样的问题,而在以前的通话里面基本上没有这些问题。
(5)声音浑浊,可懂度差
还有声音浑浊,可懂度差的问题,现在的实时通话场景比以前复杂的多,假如是在重混响的场景下,或者采集设备很差的环境下面通话,就容易导致声音的音质比较差。
(6)音频卡顿
还有像声音卡顿的问题,这个是所有使用VoIP通话过程当中大家都容易经历到的。声音卡顿大家第一时间会想到是和网络相关,但是实际解决问题的过程当中,我们发现有很多的原因都有可能导致音频卡顿。网络虽然占了很大一块,但不是所有的原因。
比如在信源质量差的时候进行声音信号处理的过程中会出现卡顿,因为一些很小的语音会被当成噪声消掉。同样,CPU过载,播放线程同步失效也会导致卡段,处理回声采集播放不同步的时候,导致漏回声的现象也会出现卡顿。所以在会议过程当中,会有来自很多方面的原因,导致最后的音质受损。
(7)宽带语音变窄带语音
另外我们还发现了一个很有意思的现象,我们公司内部很多在使用IP电话,话机和话机之间的通信音质通常比较好,但是一旦切入到腾讯会议就会发现话音由原来宽带的变成了窄带。
为什么会这样?很多时候是跟我们公司IP系统采用的网络拓扑结构有很大关系。因为很多公司内部很多网段并不能实现互联互通,这个时候往往需要经过转码,提供转码服务的语音网关为了保证最大的兼容性,往往会将原来高品质的语音通话直接转码成G.711,这个是三四十年前使用的窄带标准,能保证最大的兼容性,所有话机和系统都支持,但是音质相应的也会变成窄带的了。
宽带的语音、窄带语音,以及房间的重混响,都会导致音质受损,而且我们发现重混响对人耳的影响跟整个音量大小有关系,当你觉得音量不适合或者过响的时候,那么在重混响的房间里音质可能会进一步受损,再加上卡顿或者嘈杂声等多种因素聚合一块儿的时候,基于VoIP的通话音质就会受到很大挑战。
2. 同地多设备入会挑战
在使用腾讯会议的过程当中,还会出现同地多设备的问题。在以前使用电话的场景下,大家基本不会碰到这样的问题,因为一个房间就一个电话,不存在多个电话、多个声学设备在同一个地方入会的情形。现在随着会议解决方案的普及,每个人电脑上面都能安装一个协同会议的客户端,大家习惯性带着电脑参加会议,分享屏幕和PPT内容。每个人都进入会议,把他的屏幕分享打开,一下子会发现,在一个会议室里面出现了很多个终端在同一个房间入会,同样多个声学设备在同一个地方入会,立刻带来问题就是有回声。
对于单个设备来说,可以捕捉到播放信号作为参考,进而解决回声问题。但是对于多个设备来说,比如我这台笔记本的麦克风处理程序是怎么也不可能拿到另外一个人的扬声器播放出来的声音参考信号的,由于网络延时和当时CPU的情况不一样,这么做是不现实的。所以通常只能在本机解决简单的回声问题,对同样房间多个声源设备播放的声音没有很好办法处理。稍微好一点的情况就是产生漏回声,差一点的就会直接产生啸叫。
腾讯会议有一个检测方案,我们利用多个设备互相存在的相关性,解决这样一个同地多设备入会的问题,下文还会详细展开。
三、AI技术提升会议音频体验
在腾讯会议里面,我们还采用了什么样方法,来提高用户的通话体验呢?
1. 音频领域的超分—频宽拓展
第一,我们在通讯会议里针对一些窄带语音,特别是来自PSTN的窄带语音,做了窄带到宽带超分辨率扩展。
因为传统的PSTN电话,音质频率上限是3.4KHZ,人耳的直接听感就是声音不够明亮,声音细节不够丰富,跟VoIP电话比起来,显得差强人意。借助AI技术,根据低频的信息进行预测生成,把高频的分量很好的补偿出来,让原来听起来比较沉闷,不够丰富的语音变得更加明亮,声音音质变得更加丰满。
2. 丢包隐藏技术
第二,借助人工智能解决IP网络里面临的丢包挑战,丢包这个问题本身有很多种解决方案,在传输层面可以解决,通过FEC方案在网络层面都可以解决。但是网络层面解决丢包问题本身局限性,不管是ARQ还是FEC方案都会伴随着带宽的增加或者是延时的增加,造成不好的体验。
在声学层面上,语音信号或者是语言帧之间是存在一定的相关性的。正常人说话的时候,一个字节大概时长为200毫秒,假设一秒最多说五个字,每个字段时长为200毫秒,对于我们语音帧来说,以20毫秒为单位时长进行编包。通过丢包隐藏技术,并不需要每一个包都要收到,丢的语音包只要不是特别多的像突发大批量的丢包,而只是零星的丢包,或者是网络抖动带来的丢包情况,都可以在声学上通过数字信号处理技术和机器深度学习的技术把这些丢包弥补还原出来。
这样在对语音帧的参数进行编码的时候,我们可以通过一些数字信号处理技术和深度学习技术把丢失的参数预测出来,在信号层面通过各种滤波器把丢失掉的信号合成出来,再跟网络传输层本身的FEC或者AIQ技术结合起来,可以很好解决网络上丢包和抖动的挑战。
3. 降噪语言增强
语音通信另外一个很强的需求就是降噪,大家都不想听到环境噪声,最想关注的就是语音本身。传统的降噪技术,经过了三四十年的发展,不管是基于统计学或者是其他的方法已经可以很好的解决传统平稳噪声的降噪,能够准确估计出平稳噪声。
但是对于现在常见的非平稳,突发的声音的降噪,经典的语音处理技术就相形见绌了。腾讯会议音频解决方案是利用机器学习方法来训练模型,不断学习突发噪声本身具有的特性,如噪声频谱特性等,最终很好的把这些传统的数字信号技术解决不了的如键盘声、鼠标声、喝水水杯声、手机震动声等等这些突发的声音消除掉。
4. 语音音乐分类器
另外会议需要考虑音乐的存在场景,比如老师给学生讲课,时常会做一些视频内容的分享,这个时候就会存在高品质的背景音乐出现。如果我们的方案仅仅能处理语音,却不能处理音乐,对我们的一些应用场景就会有比较大的限制,所以如下图所示,我们研发了这样的语音音乐分类器,能够很好的将背景音乐集成到会议音频中去。
四、音频质量评估体系
对于像腾讯会议这样支持上千万DAU的互联网产品来说,对于音频的实时监控和音质评估是非常重要的。我们在整个腾讯会议开发期间,很大程度上借鉴实施了基于ITO国际电信联盟对于通信音质的测试评估方案,如下图所示,在音质测试评估方案中,我们配备了标准的人工头,标准的参考设备,来对整体语音通话的音质进行测试和评估。
整套评估方案我们参考了ITU,3GPP的标准,对在不同的声源环境,不同的测试码流,不同的声源条件下,各种不同的测试场景都有完整的定义,对于单向的语音通话,双讲,消除漏回声,降噪,评估语音SMOS和NMOS分数都有相应的标准。
如何对腾讯会议处理过的音质信号进行打分,怎样判断音质是否满足要求?我们已经形成了一整套完整的语音质量评估体系,来对整个端到端的语音通信质量进行评估。
以前在整个语音通话过程当中,无参考的音质评估普遍基于QoS参数模型的评估方案,更多是从使用的编码器类型,通话过程当中是否有丢包,延迟多少,整个音质使用的码流是多少,这些点出发,再根据参数推导出整个通话过程中的音质是怎样的。
这种方案对于运营商或者网络规划部门比较有用,因为他可以拿到这些参数。对于用户来说,就没有那样的直观感受了。
对于用户来说,能直观感受到的就是:是否存在漏回声,语音通话是否连续,通话音质是否自然等等。对于用户来说更多会关注QoE角度,从个人体验角度来看整个通话体验是否得到满意。我们把QoE指标进一步细化,主要看通话过程中的嘈杂声程度,整个通话语音的色彩度(通话语音的自然度),是否有变声和机械音,或者其他听起来不自然的声音,以及整个通话过程中语音是否存在卡顿?
人讲话本身是有卡顿的,我说一个字后会短暂停一下再说下一个字。这种卡顿跟网络丢包和网络抖动带来的卡顿是有明显区别的,我们通过数字信号处理方案和机器学习技术从QoE这三个不同维度,对音频进行无参考语音通信打分,这样就能从现网上得知,用户使用的通信会议效果是怎样的。如下图所示,用我们的无参考打分模型,跟有参考的数据进行拟合,可以看出,拟合的程度非常高。
基于无参考语音通话模型我们对现网通话质量可以有较好的把握,不需要拿到具体某一个语音的参考信号,仅仅根据播放端收到的信号,就能知道通话质量现在是否正常,如果不正常问题大概出现在什么地方。
五、会议音频系统的未来展望
在会议音频领域,除了通话以外,还有关于会议转录的需求。
微软2019年年初宣布—Project Denmark,可以用手机和Pad采集不同会议讲话人的声音,并且把不同讲话人声音进行分离。我们知道,在一个会议室多个人同时说话,讲话人声音单纯用ASR进行语音识别是无法实现的。最理想方法是把不同讲话人分离出来,再分别接ASR的后端进行语音到文字的转换。
一旦语音转成文字以后,后面就可以做很多事情,比如生成会议纪要,对内容进行检索,可以邮件发出来给没有参加会议的人浏览观看等等。
思科也在做同样的事情,思科近期收购了一家公司,这个公司也是做会议内容转录。
但是会议人声转录这里面会存在几个问题:ASR识别。ASR识别提供了很多很好的语言识别解决方案,比如对方言的识别,对基础的专有名词的识别,ASR也提供了比较好的方案前后端进行调试。
对于同一房间多人开会的会议音频转录来说最大挑战是:如何在多人会议场景下对连续说话人进行检测和切换?假如我说话的时候被别人打断了,或者是两个人讲话的声音重叠在一起,这个时候怎么有效把声音进行切割分离呢?如果多人说话在时间线上不相关,这个时候切割相对是比较容易的,通过声音识别把不同讲话人识别出来就可以了。
但是如果他们说话有重叠的时候怎么进一步分离呢?包括切割出来信号怎么进行聚类,刚才讲了几句,后面又讲几句,中间又插进来一些别的人说话,怎么把我之前讲的和之后讲的话聚合到一起?这些相关的技术对于整个会议转录来说都是非常重要的,目前有很多公司也在这方面加大投入,腾讯也有在做这样的事情。
除了会议转录需求之外,整个VoIP技术也是在不断的演进过程当中。常常听到有人问:整个5G对于语音通讯意味着什么?有人觉得语音5G带宽那么大,语音通话带宽这么小,没有太大意义。
其实不然,5G其实会为VoIP技术提供更大更好的舞台。首先是带宽对于会议语音通讯的推动作用,虽然语音本身的带宽很小,只有几十kbps,但是对于会议音频来说情况远比这个复杂。会议当中除了传输语音之外,还可以传输高品质的音频,高品质的音频就不是十几K可以搞定的。会议讲话人也可能不只是一路,会议当中同时开麦就会有好几路产生,这种情况下对于会议音频的带宽消耗是很快的,在网络条件不允许的情况下就有可能导致网络拥塞。而5G一旦把带宽上限拉大以后,会为会议音频提供更大更好的舞台,我们可以提供更优质和更高品质的音质。
5G也可以极大改善延时,几百毫秒的延时其实很大一部分都是消耗在传输延时上,但是5G可以令传输延时降低到原来的十分之一,对于整个实时可交互性体验是很大的提高。
所以5G技术的发展,能为语音通话更好的声音体验,更沉浸式的体验。只要带宽不受限制,让在会议音频上实现基于AR、VR带来的沉浸式体验成为可能,当延时大幅度缩减以后,会议交互性也会更好。如果交互性能更进一步提高,其实跟人面对面沟通就没有太大的区别了,这就是技术带来的发展。
从整个商业角度来说,我们看到很多的变化正在发生。像融合通信,更多是作为service被越来越多场景使用,现在越来越少的人采用电话设备,都是采用云的方式,因为带来的初始成本降低是非常显著的。
人工智能技术未来也会为语音通讯带来越来越好的体验,如前文提到的智能降噪、智能丢包补偿技术就可以很好解决原来的一些问题,进而提供比原来PSTN网络更好的音质体验。
WebRTC技术也将会得到普及,WebRTC也有一整套的协议族,在浏览器里得到普遍支持以后,VoIP技术借助WebRTC能在很多场景里得到广泛的应用。因为VoIP技术得到广泛的普及,在In-app communications里的应用也会越来越多。
IoT领域VoIP技术也出现了上升趋势,家里的智能音箱、智能冰箱等设备未来都会带一些通讯功能,通过IP网络进行连接。
Smarter VoIP assistants技术也会得到更多的发展, Smarter VoIP assistants是基于VoIP通话过程中提供的人工智能语音助手,来解决通讯过程中的语音问题。
六、Q&A
Q: 老师关于实时音视频通信可以推荐经典的书和开源项目吗?
A: WebRTC就是很好的开源项目,基于WebRTC书籍也有,在网络上搜索WebRTC也有很好的博客,关于WebRTC架构,里面核心的技术都有比较好的介绍,上网可以搜到。
Q: 关于本地多设备的解决方案,能详细讲解一下吗?
A: 本地多设备是这样,虽然本机的采集可以拿到本机的信号,从而可以做回声抵消,但是本地的采集是不可能拿到房间里面另外一个设备的播放信号的,这是同地多设备问题的核心所在。我们虽然不能拿到另外一个设备的播放信号作为参考,但是这个本地播放设备,跟同房间另外一个播放设备之间存在很强的相关性。因为他们都来自于同样的声源,只是经过不同的网络,不同的设备播放出来的时候,会有不同的失真和延时。所以我们不一定能做到同地多设备导致的啸叫或者回声抑制,但是一定做到同地多设备的检测,一旦检测同地多设备的时候,就可以用不同的产品策略来解决这个问题。因为同地多设备消除是非常困难,假如有三五个设备同时入会,打开麦克风,这简直就是灾难,要解决这个问题带来声学挑战对于CPU消耗会非常大,很不值得,所以做好检测就可以了。
Q: 很多直播间都在使用WebRTC,老师谈谈WebRTC是否有发展前景?
A: WebRTC很有发展前景,它首先是开源项目。WebRTC在实时音视频传输的时候,特别是对于网络NAT技术,网络穿越技术解决方案上都有很独到的地方。WebRTC对于音视频本身的编解码,音频的前处理都有一些相关的方案,WebRTC在很多场景都是很不错的解决方案。
Q: 重混响失音,怎么样提高语音清晰度?
A: 第一是多通道采集。使用麦克风阵列技术,通过方向性,比如说我在这个房间讲话,我的声音经过墙壁和桌子反射以后会被麦克风采集,造成干扰。如果麦克风是阵列形式,就可以很好对讲话人进行声源追踪,尽量只采集我的直达声,而屏蔽掉来自墙壁和桌面的反射声,这样可以很好的解决重混响问题。对于单通道麦克风的声音采集,不管是经典的数字信号处理技术,还是机器学习都可以解决这个问题,但因为毕竟是一个过滤处理,有可能会导致音质受损,所以在单通道条件下去做混响处理,并不是一件很容易的事。
Q: VoIP和VoLTE相比,有什么优缺点?
A: VoIP和VoLTE走的思路不一样。VoLTE传输的音视频流,需要QoS保障,语音比较高,发生网络拥塞优先传输语音,数据可以等等,差几十毫秒没有关系。所以VoLTE一定是保证带宽,保证低延时的。从QoS角度来讲,VoLTE有一定优势,但是当5G带宽高速公路越来越好之后,会发现VoLTE和VoIP相比就没有太多优势了。随着未来5G的大规模普及,VoIP质量可以做得非常好。
Q: 老师,出现卡顿时的具体解决的方法是什么?
A: 出现卡顿具体解决方案有很多,关键要看卡顿的具体原因是什么。是网络导致的卡顿,还是设备本身导致的卡顿,如果是网络导致的卡顿就要看是网络丢包导致还是抖动导致的,FEC技术可以解决一定的丢包问题,如果是抖动过大,就把Jitter包放大一点,虽然延时受损,但是可以解决抖动带来的卡顿。如果是设备本身有问题,可能是CPU占用率过高,调度不过来。有时候信源也会导致卡顿,比如我突然转过头说话,麦克风定向采集我的讲话声音和原先声音不匹配,这个时候就会突然听到声音变小,后台音效处理也会出现卡顿,所以卡顿原因比较复杂,需要分析原因有针对性的加以解决。
Q: 大型直播,比如赛事比赛,发布会等直播,主要是用hls、flv等,5G时代是否可以用WebRTC技术呢?
A: 两个场景不一样,直播的时候可能会跳动,或者VOD播放的时候如果延时比较大也没有关系,延时超过200毫秒,500毫秒,甚至1秒都没事,直播虽然晚一秒也不妨碍观看和体验。但是实时语音通信就不可以,超过300毫秒,甚至打电话1秒之后才回过来这肯定不行。我不觉得它们会用RTC技术,它们还是会用RTMP推流,或者HLS切包发送这样的技术,因为虽然会带来延时,但是在网络抖动处理,包括其他很多方面都能处理得更好。所以适用的场景不一样,未来做不同技术的考虑点也会不一样。
Q: 同地多设备没有办法拿到其他设备的参考声音,通过什么办法做到回声消除?
A: 同地多设备是没有拿到其他设备的参考声音,但是实际上采集声音之间还是存在一定的相关性的,在算法上可以做出判断和处理。
Q: 深度学习算法对于音频前处理相对于以前传统的方法有什么区别?
A: 有区别,传统的数字信号处理方法在不同的场景下很难做到精准的定位,比如一些传统的数字信号处理技术,对于突发的噪声没有很好的处理办法。但是这种非线性的声音用深度学习算法可以处理得很好,在拟合的时候能够把传统方式处理不好的问题,如残留回声、突发噪声、降噪问题包括聚合的问题更好的解决。
Q: 腾讯会议是在WebRTC框架吗?
A: 不是,腾讯会议不是在WebRTC框架下开发的。
Q: IoT应用就是智能家具产品应用吗?
A: 是,越来越多智能家具会使用IoT技术,如智能音箱等未来更多也会集成语音通信的技术。
Q: 语音问题是一直存在的,很好奇腾讯会议是通过什么来收集和了解到那些问题的?一个在线的视频语音产品怎么监测用户语音的视频质量?
A: 我们需要无参考语音评估系统,有了无参考语音评估系统,就可以知道现网通信当中的语言质量是怎么样的,是否存在问题,是什么样的问题,问题出现在哪个区域、哪个时间段,或者发生在哪个外设上等等。
Q: 对声源定位,麦克风阵列有什么好的分享吗?
A: 声源定位,麦克风阵列上有很多技术可以做,如DOA技术,麦克风阵列技术,传统算法都是用来做语音信号处理的,上面有很多引申的技术发展出来,具体可以参考谷歌上的详细介绍,回答得更有深度,我这里粗粗介绍一下。
Q: 音频质量的主观、客评估手段用哪个参数来评估比较合适?
A: 主观评估就是召集人来打分,对于客观评估,ITO对应有一个P863标准,参考这样的语音标准对客观指标进行打分,可以更进一步评估噪声卡顿,语音质量等。
Q: 老师,关于丢包处理补偿处理,之前学校通信课程上老师有讲过交叉帧处理的方式然后让丢失的包分布在各个帧,利用帧数据之间的关联来补偿丢包?腾讯会议的丢包处理也是类似这样的处理吗,深度学习处理的大体思路是什么呢?
A: 学校老师在课堂讲的是针对突发大丢包的情况,把包分散到各个不同分组里面,收到组里面突发丢失的那一块以后可以通过FEC技术将收到包复原出来。和这里不太一样,分组交织可以解决一定的丢包问题,但是代价是延时过大,你把一个包或者多个包分到不同组,交织开来,收集的时候必须等所有包都收集完以后,才能把语音流复原出来,这样就会带来语言延时过大的问题。
Q: 穿透转发服务器搭建方面,腾讯能提供服务吗?
A: 关于WebRTC提供的穿越技术,腾讯云也提供解决方案,但是腾讯会议使用的相关技术是供腾讯会议使用的,如果在你的解决方案里需要腾讯云提供针对网络穿越的NAT相关技术,是可以做到的。
Q: 请问质量评估是否可以这样做:本地进行抽样,然后异步传送(因为不需要实时,所以可以直接用TCP发送)给服务端,服务端对同样区间的实时音频流的数据进行抽样,来作对比。
A: 在测试过程当中可以做,在现网当中当然也可以做,但是本身抽样会有很大局限性。像腾讯会议这样千万级DAU的产品,不太可能进行抽样,抽样对于评价现网也有很大局限性,我们更多建议通过无参考质量评估的手段搭建模型,对现网所有的数据进行实时评估。
讲师简介
商世东 ,腾讯多媒体实验室高级总监,于2019年初加入腾讯多媒体实验室,担任多媒体实验室音频技术中心高级总监。加入腾讯前,商世东于2010年组建了杜比北京工程团队,任职杜比北京和悉尼工程团队高级总监9年。加入腾讯后,带领多媒体实验室音频技术中心,负责实时音视频SDK中的音频引擎,音频处理的设计和开发工作。
关注云加社区公众号,回复“在线沙龙”,即可获取老师演讲PPT~
多媒体
2020-03-25 09:54:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
写在前面
静态的插件参数
前面已经讲到,我们在QML端只有 Map 对象,各种各样的地图源是靠配置 Qt 的 LBS 插件来实现的;在配置插件的同时,还可以配置插件参数 PluginParameter ,官方LBS插件能够配置的插件参数参见 Plugin References and Parameters 。以官方的 Open Street Map Plugin 为例: Plugin { name: "osm" PluginParameter { name: "osm.useragent"; value: "My great Qt OSM application" } PluginParameter { name: "osm.mapping.host"; value: "http://osm.tile.server.address/" } PluginParameter { name: "osm.mapping.copyright"; value: "All mine" } PluginParameter { name: "osm.routing.host"; value: "http://osrm.server.address/viaroute" } PluginParameter { name: "osm.geocoding.host"; value: "http://geocoding.server.address" } }
除了可以为LBS插件配置一些代理的参数以外,比较常用的就是动态缓存目录 osm.mapping.cache.directory 和离线缓存目录 osm.mapping.offline.directory 的配置,离线缓存目录的参数配置目前只在 Open Street Map Plugin 看到有。
在QML文档中配置了插件参数之后,LBS工厂都会将其打包成一个QVariantMap类型的parameters,在后端用来构造QGeoTiledMappingManagerEngine及其派生类型。所以,自然而然的,我们实现自己的LBS插件的时候,也可以定义从前端的QML文档配置的插件参数,然后在继承QGeoTiledMappingManagerEngine实现的派生类型的构造函数中,将这些参数的键值对解析出来,进行相应的操作。例如在QGeoTiledMappingManagerEngineOsm的构造函数中,我们可以看到如下解析并配置缓存目录的代码: /* TILE CACHE */ if (parameters.contains(QStringLiteral("osm.mapping.cache.directory"))) { m_cacheDirectory = parameters.value(QStringLiteral("osm.mapping.cache.directory")).toString(); } else { // managerName() is not yet set, we have to hardcode the plugin name below m_cacheDirectory = QAbstractGeoTileCache::baseLocationCacheDirectory() + QLatin1String(pluginName); } if (parameters.contains(QStringLiteral("osm.mapping.offline.directory"))) m_offlineDirectory = parameters.value(QStringLiteral("osm.mapping.offline.directory")).toString(); QGeoFileTileCacheOsm *tileCache = new QGeoFileTileCacheOsm(m_providers, m_offlineDirectory, m_cacheDirectory);
动态的地图参数
动态的地图参数,在Qt 5.9中作为技术预览,到Qt Location 5.11的时候 MapParameter 已经改名为 DynamicParameter 了。 MapParameter 是通过 Map 对象的JS接口方法 addMapParameter 进行添加的,添加的地图参数集合可以通过 Map 对象的 mapParameters 属性访问到,例如: Map { id: map plugin: "xxx" MapParameter { id: mapParameter type: "xxx" } onMapReadyChanged: map.addMapParameter(mapParameter) }
利用动态的地图参数特性,我们可以在自己实现的LBS插件上,额外扩展一些前后端的交互功能,例如地图瓦片的离线缓存功能。
实现地图离线缓存
我们在使用地图的时候常常有一些离线场景,比如无人机在野外作业时地面站软件需要离线地图,这时就需要实现地图瓦片数据的离线缓存管理。
一、利用静态的插件参数进行默认的配置
我们在实例化地图插件的时候,可以允许前端通过“[xxx].mapping.offline.directory”参数,对地图的离线缓存目录进行配置,其对应在后端QGeoTiledMappingManagerEngine的派生类构造函数中解析: // Parse cache offline directory from parameter. if(parameters.contains(QStringLiteral("[xxx].mapping.offline.directory"))) { m_offlineDirectory = parameters.value( QStringLiteral("[xxx].mapping.offline.directory")).toString(); } else // Set default offline directory { QString oldLocation = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation); QString newLocation = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation); m_offlineDirectory = m_cacheDirectory.replace(oldLocation, newLocation); }
二、利用动态的地图参数进行前后端交互
我们需要指定一个地理范围(例如通过按住鼠标左键在地图上框选出一个BoundingBox),然后指定瓦片地图的缩放级别(例如弹出表单让用户填写1-10级的ZoomRange),这时我们还需要点击“OK”按钮去执行离线缓存Task,那么这些参数和执行动作的触发信息要如何传递到后端呢?这就需要使用 addMapParameter 来帮助我们。
QML的对象是支持属性扩展的(事实上QObject都支持属性扩展 -_-|| ),我们可以对一个 MapParameter 的具体实例进行扩展,例如: MapParameter { id: offlineCache type: "[xxx].mapping.offline.settings" property string directory: "[your offline cache directory]" property var boundingBox: QtPositioning.rectangle( QtPositioning.coordinate("[top left corner]"), QtPositioning.coordinate("[bottom right corner]")) property int minZoom: 0 property int maxZoom: 10 }
我们在后端继承QGeoTiledMapPrivate实现QGeoTiledMap[XXX]Private类型时,就可在 addMapParameter 方法里解析该参数,并具体实现离线缓存功能: void addParameter(QGeoMapParameter* param) override { Q_Q(QGeoTiledMap[XXX]); static const QStringList acceptedParameterTypes = QStringList() << QStringLiteral("[xxx].mapping.offline.settings"); QVariantMap kvm = param->toVariantMap(); switch (acceptedParameterTypes.indexOf(param->type())) { default: { qWarning() << "[XXX]Map: Invalid value for property 'type' " + param->type(); break; } case 0: { QString directory; QVector zoomRange; QGeoRectangle boundingBox; // Parse the "[xxx].mapping.offline.settings" parameters. if(kvm.contains("directory")) directory = kvm["directory"].toString(); else qWarning() << "[XXX]Map: Can not catch the offline cache directory."; if(kvm.contains("boundingBox")) boundingBox = kvm["boundingBox"].value(); else qWarning() << "[XXX]Map: Can not catch the bounding box."; if(kvm.contains("minZoom") && kvm.contains("maxZoom")) { bool minZoomOk = false, maxZoomOk = false; int minZoom = qMax(m_minZoomLevel, kvm["minZoom"].toInt(&minZoomOk)); int maxZoom = qMin(m_maxZoomLevel, kvm["maxZoom"].toInt(&maxZoomOk)); if(minZoomOk && maxZoomOk) { for(int zIt = minZoom; zIt <= maxZoom; zIt++) zoomRange.append(zIt); } else qWarning() << "[XXX]Map: Can not catch the zooms when fetch offline cache."; // To call the fetch offline cache function. q->fetchOfflineCache(boundingBox, zoomRange, directory); } break; } }
当前端将该地图参数动态地加入到地图中的时候,就会触发上述方法,并执行到我们具体实现的fetchOfflineCache方法;当然我们还可以在removeParameter方法中具体实现cancelOfflineCache,而像loadOfflineCache、saveOfflineCache等方法,也皆可通过 addMapParameter 实现动态调用。
三、模仿瓦片文件缓存机制实现离线缓存
fetchOfflineCache的具体实现思路是:(1)遍历每一个需要离线缓存的Zoom级别,利用QGeoCameraTiles计算出所需的瓦片参数队列;(2)检查已经存在于文件缓存中的瓦片参数,组成需要从文件缓存中复制的队列,其余的瓦片参数组成需要从网络下载的队列;(3)遍历需要从文件缓存中复制的队列,将瓦片复制到离线缓存目录;(4)启动一个计时器,去依次请求下载队列里的瓦片,下载成功后存放到离线缓存目录。 // fetchOfflineCache function : ------------------------------------------------------------------- // Get camera data QGeoCameraData cameraData = d->m_visibleTiles->cameraData(); QGeoCoordinate center = boundingBox.center(); cameraData.setCenter(center); // Create camera tiles factory QGeoCameraTiles cameraTiles; cameraTiles.setMapType(d->m_visibleTiles->activeMapType()); // 1 cameraTiles.setTileSize(d->m_visibleTiles->tileSize()); // 2 cameraTiles.setMapVersion(d->m_tileEngine->tileVersion()); // 3 cameraTiles.setViewExpansion(1.0); // 4 QString pluginString(d->m_engine->managerName() + QLatin1Char('_') + QString::number(d->m_engine->managerVersion())); cameraTiles.setPluginString(pluginString); // 5 // Prepare queues to fetch tiles QList createCopyQueue; for(int i = 0; i < zoomRange.length(); i++) { int currentIntZoom = zoomRange.at(i); cameraData.setZoomLevel(currentIntZoom); QGeoProjectionWebMercator projection; projection.setCameraData(cameraData, true); QPointF topLeft = projection.coordinateToItemPosition(boundingBox.topLeft(), false).toPointF(); QPointF bottomRight = projection.coordinateToItemPosition(boundingBox.bottomRight(), false).toPointF(); QRectF selectRect = QRectF(topLeft, bottomRight); QSize selectSize = selectRect.size().toSize(); if(selectSize.isNull()) selectSize = QSize(1, 1); // At least one pixel! cameraTiles.setScreenSize(selectSize); // 6 cameraTiles.setCameraData(cameraData); // 7 // Create all tiles QSet tiles = cameraTiles.createTiles(); // Check tiles in cache directory for(const QGeoTileSpec& tile : qAsConst(tiles)) { if(!d->m_tileCache->offlineStorageContains(tile)) // isn't in offline cache { if(d->m_tileCache->diskCacheContains(tile)) createCopyQueue << tile; // copy queue from disk cache to offline cache else d->m_downloadQueue << tile; // add to download queue } } } d->m_progressTotal = d->m_downloadQueue.count() + createCopyQueue.count(); if(d->m_downloadQueue.count() == 0 && createCopyQueue.count() == 0) { int percentage = 100; QString message = QStringLiteral("All tiles needed to fetch have been in offline storage."); emit fetchTilesProgressChanged(percentage, message); return; } // Copy tiles from cache directory to offline directory for(const QGeoTileSpec& tile : qAsConst(createCopyQueue)) { int percentage = 100 * (++d->m_progressValue) / d->m_progressTotal; const QString tileName = tileSpecToName(tile); QString message; if(d->m_tileCache->addToOfflineStorage(tile)) message = QString("Fetched the %1 from disk cache successfully.").arg(tileName); else message = QString("Fetched the %1 from disk cache unsuccessfully.").arg(tileName); emit fetchTilesProgressChanged(percentage, message); } // Start a timer to request tiles online if(!d->m_downloadQueue.isEmpty() && !d->m_timer.isActive()) d->m_timer.start(0, this); // timerEvent function: --------------------------------------------------------------------------- Q_D(QGeoTiledMap[XXX]); if (event->timerId() != d->m_timer.timerId()) { QObject::timerEvent(event); return; } QMutexLocker ml(&d->m_queueMutex); if (d->m_downloadQueue.isEmpty()) { d->m_timer.stop(); return; } ml.unlock(); // request the next tile requestNextTile();
其中从网络下载瓦片的部分,参考QGeoTileFetcher进行实现;带有离线缓存管理的QGeoFileTileCache[XXX]类型,参考官方 Open Street Map Plugin 的QGeoFileTileCacheOsm去实现。
多媒体
2020-03-23 03:24:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
原创不易,转载请附地址: https://my.oschina.net/ffintell/blog/3207641
1 什么是数字图像
一幅图像可定义为一个二维函数f(x,y),其中x和y是空间(平面)坐标,而任何一对空间坐标(x,y)处的幅值f称为图像在该点处的强度或灰度。当x,y和灰度值f是有限的离散数值时,我们称该图像为数字图像。数字图像处理是指借助于数字计算机来处理数字图像。
注意:数字图像是由有限数量的元素组成的,每个元素都有一个特定的位置和幅值,这些元素称为像素。
数字图像处理方法的重要性源于两个主要应用领域:
改善图示信息以便人们解释;为存储、传输和表示而对图像数据进行处理,以便于机器自动理解 。
数字图像有三种典型处理:低级、中级、高级
低级:输入图像输出图像。例:图像降噪预处理、对比度增强、图像锐化等。
中级:输入图像输出特征。例:图像分割减少描述、输出边缘轮廓、物体的HOG特征,LAB特征等。
高级:输入图像,涉及“理解”已识别目标的总体,在连续统一体的远端执行与视觉相关的功能。例:行人识别等。 从图像处理到计算机视觉是一个连续统一体。
2 数字图像处理方向领域
图像增强: 图像增强和复原的目的是为了提高图像的质量,如去除噪声,提高图像的清晰度等。图像增强不考虑图像降质的原因,突出图像中所感兴趣的部分。如强化图像高频分量,可使图像中物体轮廓清晰,细节明显;如强化低频分量可减少图像中噪声影响。

图像增强 图像复原
图像复原: 图像复原要求对图像降质的原因有一定的了解,一般讲应根据降质过程建立“降质模型”,再采用某种滤波方法,恢复或重建原来的图像。
图像变换: 由于图像阵列很大,直接在空间域中进行处理,涉及计算量很大。因此,往往采用各种图像变换的方法,如傅立叶变换、沃尔什变换、离散余弦变换等间接处理技术,将空间域的处理转换为变换域处理,不仅可减少计算量,而且可获得更有效的处理(如傅立叶变换可在频域中进行数字滤波处理)。 如下图,原图与二维傅里叶变换 。
傅里叶变换
图像编码压缩: 图像编码压缩技术可减少描述图像的数据量(即比特数),以便节省图像传输、处理时间和减少所占用的存储器容量。压缩可以在不失真的前提下获得,也可以在允许的失真条件下进行。编码是压缩技术中最重要的方法,它在图像处理技术中是发展最早且比较成熟的技术。
图像形态学操作: 在图像处理技术中,有一些的操作会对图像的形态发生改变,这些操作一般称之为形态学操作(phology)。数学形态学是基于集合论的图像处理方法,最早出现在生物学的形态与结构中,图像处理中的形态学操作用于图像与处理操作(去噪,形状简化)图像增强(骨架提取,细化,凸包及物体标记)、物体背景分割及物体形态量化等场景中,形态学操作的对象是二值化图像。
有名的形态学操作中包括腐蚀,膨胀,开操作,闭操作等。其中腐蚀,膨胀是许多形态学操作的基础。
图像分割: 图像分割是数字图像处理中的关键技术之一。图像分割是将图像中有意义的特征部分提取出来,其有意义的特征有图像中的边缘、区域等,这是进一步进行图像识别、分析和理解的基础。虽然已研究出不少边缘提取、区域分割的方法,但还没有一种普遍适用于各种图像的有效方法。因此,对图像分割的研究还在不断深入之中,是图像处理中研究的热点之一。
图像描述(特征选择): 图像描述是图像识别和理解的必要前提。作为最简单的二值图像可采用其几何特性描述物体的特性,一般图像的描述方法采用二维形状描述,它有边界描述和区域描述两类方法。对于特殊的纹理图像可采用二维纹理特征描述。
随着图像处理研究的深入发展,已经开始进行三维物体描述的研究,提出了体积描述、表面描述、广义圆柱体描述等方法。
图像分类(识别): 图像分类(识别)属于模式识别的范畴,其主要内容是图像经过某些预处理(增强、复原、压缩)后,进行图像分割和特征提取,从而进行判决分类。图像分类常采用经典的模式识别方法,有统计模式分类和句法(结构)模式分类,近年来新发展起来的人工神经网络模式分类在图像识别中也越来越受到重视。
3 数字图像应用领域
主要应用于航天和航空方面、生物医学工程方面、通信工程方面、工业和工程方面 、安防交通、文化艺术方面 等各行各业。
原创不易,转载请附地址: https://my.oschina.net/ffintell/blog/3207641
(示例图片来源网络,如有侵权,联系删除)
多媒体
2020-03-21 09:15:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
最近业务需求需要我们直播返回或者退出直播间时,开一个小窗口在全局继续直播视频,先看效果图。
调研了一下当下主流直播平台,斗鱼、BiliBili等app,都是用WindowManger做的(这个你可以在应用权限列表看看有没有悬浮窗权限,然后把斗鱼的权限禁止,这时候回到斗鱼直播间退出时候就会让你授权了)即通过WindowManger add一个全局的view,可以申请权限悬浮在所有应用之上以此来实现全局悬浮窗
ok,分析完实现原理我们就开始撸代码了
实现悬浮窗难点
1:权限申请:一个是6.0及以后要用户手动授权,因为悬浮窗权限属于高危权限,二是因为MIUI,底层修改了权限,所以在小米手机上需要特殊处理,还有就是8.0以后权限的定义类型变了下面有代码会详解这块
2:对于悬浮窗touch 事件的监听,比如点击事件和touch事件,如果同时监听那么setOnclickListener就没有效果了,需要区别点击和touch,还有就是拖动小窗口移动位置,这里是指针对整个窗体即设置touch事件又设置点击事件会有冲突
3:直播组件的初始化,即全局单例的直播窗口,可以是自己封装一个自定义View,这个因各自的直播SDK而定,我这用的sdk在插件里,所以实现起来比较麻烦,但是一般直播sdk(阿里云或者七牛)都可以用同一个直播组件对象,即在直播页面销毁或者返回时把对象传递到小窗口里,实现无缝衔接开启小窗口直播,不需要重新加载,这里用EventBus发个消息或者广播都可以实现
一:权限申请
首先要在清单文件即AndroidManifest文件声明 悬浮窗权限
然后我们悬浮窗触发的时机是在直播页面返回的时候,那也就是说可以在onDestory()或者finsh()时候去做权限申请
注:因为6.0以后是高危权限,所以代码是拿不到权限的,需要跳到权限申请列表让用户授权 if (isLiveShow) { if (Build.VERSION.SDK_INT >= 23) { if (!Settings.canDrawOverlays(getContext())) { //没有悬浮窗权限,跳转申请 Toast.makeText(getApplicationContext(), "请开启悬浮窗权限", Toast.LENGTH_LONG).show(); Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION); startActivity(intent); } else { initLiveWindow(); } } else { //6.0以下 只有MUI会修改权限 if (MIUI.rom()) { if (PermissionUtils.hasPermission(getContext())) { initLiveWindow(); } else { MIUI.req(getContext()); } } else { initLiveWindow(); } } }
而低版本一般是不需要用户授权的除了MIUI,所以我们需要先判断是否是MIUI系统,然后判断MIUI版本,然后不同的版本对应不同的权限申请姿势,如果你不这么做,那么恭喜你在低版本(低于6.0)的小米手机上不是返回跳转权限崩溃,因为底层改了授权列表类或者是根本不会跳授权没有反应, //6.0以下 只有MUI会修改权限 if (MIUI.rom()) { if (PermissionUtils.hasPermission(getContext())) { initLiveWindow(); } else { MIUI.req(getContext()); } } else { initLiveWindow(); }
先判断是否是MIUI系统 public static boolean rom() { return Build.MANUFACTURER.equals("Xiaomi"); }
然后根据不同版本,不同的授权姿势 /** * Description: * Created by PangHaHa on 18-7-25. * Copyright (c) 2018 PangHaHa All rights reserved. * * /** *

* 需要清楚:一个MIUI版本对应小米各种机型,基于不同的安卓版本,但是权限设置页跟MIUI版本有关 * 测试TYPE_TOAST类型: * 7.0: * 小米 5 MIUI8 -------------------- 失败 * 小米 Note2 MIUI9 -------------------- 失败 * 6.0.1 * 小米 5 -------------------- 失败 * 小米 红米note3 -------------------- 失败 * 6.0: * 小米 5 -------------------- 成功 * 小米 红米4A MIUI8 -------------------- 成功 * 小米 红米Pro MIUI7 -------------------- 成功 * 小米 红米Note4 MIUI8 -------------------- 失败 *

* 经过各种横向纵向测试对比,得出一个结论,就是小米对TYPE_TOAST的处理机制毫无规律可言! * 跟Android版本无关,跟MIUI版本无关,addView方法也不报错 * 所以最后对小米6.0以上的适配方法是:不使用 TYPE_TOAST 类型,统一申请权限 */ public class MIUI { private static final String miui = "ro.miui.ui.version.name"; private static final String miui5 = "V5"; private static final String miui6 = "V6"; private static final String miui7 = "V7"; private static final String miui8 = "V8"; private static final String miui9 = "V9"; public static boolean rom() { return Build.MANUFACTURER.equals("Xiaomi"); } private static String getProp() { return Rom.getProp(miui); } public static void req(final Context context) { switch (getProp()) { case miui5: reqForMiui5(context); break; case miui6: case miui7: reqForMiui67(context); break; case miui8: case miui9: reqForMiui89(context); break; } } private static void reqForMiui5(Context context) { String packageName = context.getPackageName(); Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); Uri uri = Uri.fromParts("package", packageName, null); intent.setData(uri); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); if (isIntentAvailable(intent, context)) { context.startActivity(intent); } } private static void reqForMiui67(Context context) { Intent intent = new Intent("miui.intent.action.APP_PERM_EDITOR"); intent.setClassName("com.miui.securitycenter", "com.miui.permcenter.permissions.AppPermissionsEditorActivity"); intent.putExtra("extra_pkgname", context.getPackageName()); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); if (isIntentAvailable(intent, context)) { context.startActivity(intent); } } private static void reqForMiui89(Context context) { Intent intent = new Intent("miui.intent.action.APP_PERM_EDITOR"); intent.setClassName("com.miui.securitycenter", "com.miui.permcenter.permissions.PermissionsEditorActivity"); intent.putExtra("extra_pkgname", context.getPackageName()); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); if (isIntentAvailable(intent, context)) { context.startActivity(intent); } else { intent = new Intent("miui.intent.action.APP_PERM_EDITOR"); intent.setPackage("com.miui.securitycenter"); intent.putExtra("extra_pkgname", context.getPackageName()); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); if (isIntentAvailable(intent, context)) { context.startActivity(intent); } } } /** * 有些机型在添加TYPE-TOAST类型时会自动改为TYPE_SYSTEM_ALERT,通过此方法可以屏蔽修改 * 但是...即使成功显示出悬浮窗,移动的话也会崩溃 */ private static void addViewToWindow(WindowManager wm, View view, WindowManager.LayoutParams params) { setMiUI_International(true); wm.addView(view, params); setMiUI_International(false); } private static void setMiUI_International(boolean flag) { try { Class BuildForMi = Class.forName("miui.os.Build"); Field isInternational = BuildForMi.getDeclaredField("IS_INTERNATIONAL_BUILD"); isInternational.setAccessible(true); isInternational.setBoolean(null, flag); } catch (Exception e) { e.printStackTrace(); } } }
以及利用Runtime 执行命令 getprop 来获取手机的版本型号,因为MIUI不同的版本对应的底层都不一样,毫无规律可言! public class Rom { static boolean isIntentAvailable(Intent intent, Context context) { return intent != null && context.getPackageManager().queryIntentActivities( intent, PackageManager.MATCH_DEFAULT_ONLY).size() > 0; } static String getProp(String name) { BufferedReader input = null; try { Process p = Runtime.getRuntime().exec("getprop " + name); input = new BufferedReader(new InputStreamReader(p.getInputStream()), 1024); String line = input.readLine(); input.close(); return line; } catch (IOException ex) { return null; } finally { if (input != null) { try { input.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
权限申请的工具类 public class PermissionUtils { public static boolean hasPermission(Context context) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { return Settings.canDrawOverlays(context); } else { return hasPermissionBelowMarshmallow(context); } } public static boolean hasPermissionOnActivityResult(Context context) { if (Build.VERSION.SDK_INT == Build.VERSION_CODES.O) { return hasPermissionForO(context); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { return Settings.canDrawOverlays(context); } else { return hasPermissionBelowMarshmallow(context); } } /** * 6.0以下判断是否有权限 * 理论上6.0以上才需处理权限,但有的国内rom在6.0以下就添加了权限 * 其实此方式也可以用于判断6.0以上版本,只不过有更简单的canDrawOverlays代替 */ @RequiresApi(api = Build.VERSION_CODES.KITKAT) static boolean hasPermissionBelowMarshmallow(Context context) { try { AppOpsManager manager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE); Method dispatchMethod = AppOpsManager.class.getMethod("checkOp", int.class, int.class, String.class); //AppOpsManager.OP_SYSTEM_ALERT_WINDOW = 24 return AppOpsManager.MODE_ALLOWED == (Integer) dispatchMethod.invoke( manager, 24, Binder.getCallingUid(), context.getApplicationContext().getPackageName()); } catch (Exception e) { return false; } } /** * 用于判断8.0时是否有权限,仅用于OnActivityResult * 针对8.0官方bug:在用户授予权限后Settings.canDrawOverlays或checkOp方法判断仍然返回false */ @RequiresApi(api = Build.VERSION_CODES.M) private static boolean hasPermissionForO(Context context) { try { WindowManager mgr = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); if (mgr == null) return false; View viewToAdd = new View(context); WindowManager.LayoutParams params = new WindowManager.LayoutParams(0, 0, Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ? WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY : WindowManager.LayoutParams.TYPE_SYSTEM_ALERT, WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSPARENT); viewToAdd.setLayoutParams(params); mgr.addView(viewToAdd, params); mgr.removeView(viewToAdd); return true; } catch (Exception e) { e.printStackTrace(); } return false; } }
二:弹窗的初始化,以及touch事件的监听
首先我们需要明白一点 windowManger的源码,只有三个方法 package android.view; /** Interface to let you add and remove child views to an Activity. To get an instance * of this class, call {@link android.content.Context#getSystemService(java.lang.String) Context.getSystemService()}. */ public interface ViewManager { /** * Assign the passed LayoutParams to the passed View and add the view to the window. *

Throws {@link android.view.WindowManager.BadTokenException} for certain programming * errors, such as adding a second view to a window without removing the first view. *

Throws {@link android.view.WindowManager.InvalidDisplayException} if the window is on a * secondary {@link Display} and the specified display can't be found * (see {@link android.app.Presentation}). * @param view The view to be added to this window. * @param params The LayoutParams to assign to view. */ public void addView(View view, ViewGroup.LayoutParams params); public void updateViewLayout(View view, ViewGroup.LayoutParams params); public void removeView(View view); }
看名字就知道,增加,更新,删除
然后我们需要自定义一个View 通过addView 添加到windowManger 上,先上关键代码 需要注意两点
A、8.0以后权限定义变了 需要修改type //设置type.系统提示型窗口,一般都在应用程序窗口之上. if (Build.VERSION.SDK_INT >= 26) { //8.0新特性 params.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; } else { params.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT; }
B、参考系和初始坐标的概念,参考系Gravity 即以哪点为原点而不是初始化弹窗相对于屏幕的位置!其中需要注意的是其Gravity属性: 注意:Gravity不是说你添加到WindowManager中的View相对屏幕的几种放置, 而是说你可以设置你的参考系 ! 例如:mWinParams.gravity= Gravity.LEFT | Gravity.TOP; 意思是以屏幕左上角为参考系,那么屏幕左上角的坐标就是(0,0), 这是你后面摆放View位置的唯一依据.当你设置为mWinParams.gravity = Gravity.CENTER; 那么你的屏幕中心为参考系,坐标(0,0).一般我们用屏幕左上角为参考系.
C、touch事件的处理,由于我们View先相应touch事件,之后才会传递到onClick点击事件,如果touch拦截了就不会传递到下一级了
1,我们通过手指移动后的位置,添加偏移量,然后windowManger 调用 updateViewlayout 更新界面 达到实时拖动更改位置
2,通过计算上一次触碰屏幕位置和这一次触碰屏幕的偏移量,x轴和y轴的偏移量都小于2像素,认定为点击事件,执行整个窗体的点击事件,否则执行整个窗体的touch事件 //主动计算出当前View的宽高信息. toucherLayout.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED); //处理touch toucherLayout.setOnTouchListener(new View.OnTouchListener() {@Override public boolean onTouch(View view, MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: isMoved = false; // 记录按下位置 lastX = event.getRawX(); lastY = event.getRawY(); start_X = event.getRawX(); start_Y = event.getRawY(); break; case MotionEvent.ACTION_MOVE: isMoved = true; // 记录移动后的位置 float moveX = event.getRawX(); float moveY = event.getRawY(); // 获取当前窗口的布局属性, 添加偏移量, 并更新界面, 实现移动 params.x += (int)(moveX - lastX); params.y += (int)(moveY - lastY); windowManager.updateViewLayout(toucherLayout, params); lastX = moveX; lastY = moveY; break; case MotionEvent.ACTION_UP: float fmoveX = event.getRawX(); float fmoveY = event.getRawY(); if (Math.abs(fmoveX - start_X) < offset && Math.abs(fmoveY - start_Y) < offset) { isMoved = false; remove(context); leaveCast(context); String PARAM_CIRCLE_ID = "param_circle_id"; Intent intent = new Intent(); intent.putExtra(PARAM_CIRCLE_ID, circle_id); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.setComponent(new ComponentName(RePlugin.getHostContext().getPackageName(), "com.sina.licaishicircle.sections.circledetail.CircleActivity")); context.startActivity(intent); } else { isMoved = true; } break; } // 如果是移动事件, 则消费掉; 如果不是, 则由其他处理, 比如点击 return isMoved; }
三:全局单例直播以及直播窗口的构造复用
因为项目用了360的Replugin 插件化管理方式,而且直播组件都是在插件中,需要反射获取直播弹窗工具类 public class LiveWindowUtil { private static class Hold { public static LiveWindowUtil instance = new LiveWindowUtil(); } public static LiveWindowUtil getInstance() { return Hold.instance; } public LiveWindowUtil() { //代码使用插件Fragment RePlugin.fetchContext("sina.com.cn.courseplugin"); } private Object o; private Class clazz; public void init(Context context, Map map) { try { ClassLoader classLoader = RePlugin.fetchClassLoader("sina.com.cn.courseplugin");//获取插件的ClassLoader clazz = classLoader.loadClass("sina.com.cn.courseplugin.tools.LiveUtils"); o = clazz.newInstance(); Method method = clazz.getMethod("initLive", Context.class, Map.class); method.invoke(o, context, map); }catch (NoSuchMethodException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); }catch (NullPointerException e){ e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } } public void remove(Context context) { Method method = null; try { if(clazz != null && o != null) { method = clazz.getMethod("remove", Context.class); method.invoke(o,context); } } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } } }
总结一下,主要还是需要拿到权限,然后传递直播组件复用到小窗口,监听悬浮窗的touch事件,权限的坑比较大一点除了MIUI可能别的品牌手机也会有低于6.0莫名其妙拿不到权限。
原创作者:庞哈哈12138,原文链接: https://www.jianshu.com/p/e953f5b924e1
欢迎关注我的微信公众号「码农突围」,分享Python、Java、大数据、机器学习、人工智能等技术,关注码农技术提升•职场突围•思维跃迁,20万+码农成长充电第一站,陪有梦想的你一起成长。

多媒体
2020-03-20 22:45:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
MPV 是一个基于 MPlayer 和 mplayer2 的开源极简全能播放器。支持各种视频格式、音频解码、支持特效字幕,不仅支持本地播放,同样支持网络播放。但是可惜没有图形化界面,Baka MPlayer 是个不错的图形化界面,鉴于原作者很久没有再次更新,本人稍作了些许改动,以便更适合使用,但是只是发布了Windows平台的可执行文件,其他平台没有花时间去编译出来。
变更记录如下:
New: 始终打开一个播放实例,并立即激活之前的窗口
Fixed: 窗口标题,播放某些文件名显示乱码
Changed: 拖动进度条的时候,显示的时间有缓慢滑入滑出的效果,产生延迟感,故直接剔除
Changed: 更换播放器的图标
Changed: 去掉打开另外一个播放器的菜单
Changed: 去掉在线帮助的菜单
Changed: 去掉关于页面下的乱码
Changed: 去掉检查更新的菜单和功能,调整版本号为3.0.0
Changed: 不显示播放列表
Changed: 第二个播放剩余时间,改为总时长
Windows可执行文件下载: https://raw.githubusercontent.com/wankaiming/Baka-MPlayer/master/windows/exe-file/baka-mplayer.7z
代码仓库地址: https://github.com/wankaiming/Baka-MPlayer
多媒体
2020-03-18 21:49:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
导读: 直播行业的竞争越来越激烈,进过18年这波洗牌后,已经度过了蛮荒暴力期,剩下的都是在不断追求体验。最近在帮做直播优化首开,通过多种方案并行,把首开降到500ms以下,希望能对大家有借鉴。
背景: 基于FFmpeg的ijkplayer,最新版本0.88版本。
拉流协议基于http-flv,http-flv更稳定些,国内大部分直播公司基本都是使用http-flv了,从我们实际数据来看,http-flv确实稍微更快些。但是考虑到会有rtmp源,这块也加了些优化。
IP直通车
简单理解就是,把域名替换成IP。比如 https://www.baidu.com/,你可以直接换成14.215.177.39,这样做的目的是,省去了DNS解析的耗时,尤其在网络不好时,访问域名,域名要去解析,再给你返回。不仅仅有时间解析过长的问题,还有小运营商DNS劫持的问题。一般就是在启动应用时,就开始对拉流的域名进行预解析好,存到本地,然后在真正拉流时,直接用就行。典型的案列,就是很多人使用HTTPDNS,这个github上也有开源,可以自行去研究下。
需要注意的是,这种方案在使用 HTTPS 时,是会失败的。因为 HTTPS 在证书验证的过程,会出现 domain 不匹配导致 SSL/TLS 握手不成功。
服务端 GOP 缓存
除了客户端业务侧的优化外,我们还可以从流媒体服务器侧进行优化。我们都知道直播流中的图像帧分为:I 帧、P 帧、B 帧,其中只有 I 帧是能不依赖其他帧独立完成解码的,这就意味着当播放器接收到 I 帧它能马上渲染出来,而接收到 P 帧、B 帧则需要等待依赖的帧而不能立即完成解码和渲染,这个期间就是「黑屏」了。
所以,在服务器端可以通过缓存 GOP(在 H.264 中,GOP 是封闭的,是以 I 帧开头的一组图像帧序列),保证播放端在接入直播时能先获取到 I 帧马上渲染出画面来,从而优化首屏加载的体验。
这里有一个 IDR 帧的概念需要讲一下,所有的 IDR 帧都是 I 帧,但是并不是所有 I 帧都是 IDR 帧,IDR 帧是 I 帧的子集。I 帧严格定义是帧内编码帧,由于是一个全帧压缩编码帧,通常用 I 帧表示「关键帧」。IDR 是基于 I 帧的一个扩展,带了控制逻辑,IDR 图像都是 I 帧图像,当解码器解码到 IDR 图像时,会立即将参考帧队列清空,将已解码的数据全部输出或抛弃。重新查找参数集,开始一个新的序列。这样如果前一个序列出现重大错误,在这里可以获得重新同步的机会。IDR 图像之后的图像永远不会使用 IDR 之前的图像的数据来解码。在 H.264 编码中,GOP 是封闭式的,一个 GOP 的第一帧都是 IDR 帧。
推流端设置
一般播放器需要拿到一个完整的GOP,才能记性播放。GOP是在推流端可以设置,比如下面这个图,是我dump一个流,看到的GOP情况。GOP大小是50,推流过来的fps设置是25,也就是1s内会显示25个Frame,50个Frame,刚好直播设置GOP 2S,但是直播一般fps不用设置这么高,可以随便dump任何一家直播公司的推流,设置fps在15-18之间就够了。
播放器相关耗时
当set一个源给播放器后,播放器需要open这个流,然后和服务端建立长连接,然后demux,codec,最后渲染。我们可以按照播放器的四大块,依次优化 数据请求耗时 解复用耗时 解码耗时 渲染出图耗时
数据请求
这里就是网络和协议相关。无论是http-flv,还是rtmp,都主要是基于tcp的,所以一定会有tcp三次握手,同时打开tcp.c分析。需要加日志在一些方法中,如下tcp_open方法。是已经改动过的 /* return non zero if error */ static int tcp_open(URLContext *h, const char *uri, int flags) { av_log(NULL, AV_LOG_INFO, "tcp_open begin"); ...省略部分代码 if (!dns_entry) { #ifdef HAVE_PTHREADS av_log(h, AV_LOG_INFO, "ijk_tcp_getaddrinfo_nonblock begin.\n"); ret = ijk_tcp_getaddrinfo_nonblock(hostname, portstr, &hints, &ai, s->addrinfo_timeout, &h->interrupt_callback, s->addrinfo_one_by_one); av_log(h, AV_LOG_INFO, "ijk_tcp_getaddrinfo_nonblock end.\n"); #else if (s->addrinfo_timeout > 0) av_log(h, AV_LOG_WARNING, "Ignore addrinfo_timeout without pthreads support.\n"); av_log(h, AV_LOG_INFO, "getaddrinfo begin.\n"); if (!hostname[0]) ret = getaddrinfo(NULL, portstr, &hints, &ai); else ret = getaddrinfo(hostname, portstr, &hints, &ai); av_log(h, AV_LOG_INFO, "getaddrinfo end.\n"); #endif if (ret) { av_log(h, AV_LOG_ERROR, "Failed to resolve hostname %s: %s\n", hostname, gai_strerror(ret)); return AVERROR(EIO); } cur_ai = ai; } else { av_log(NULL, AV_LOG_INFO, "Hit DNS cache hostname = %s\n", hostname); cur_ai = dns_entry->res; } restart: #if HAVE_STRUCT_SOCKADDR_IN6 // workaround for IOS9 getaddrinfo in IPv6 only network use hardcode IPv4 address can not resolve port number. if (cur_ai->ai_family == AF_INET6){ struct sockaddr_in6 * sockaddr_v6 = (struct sockaddr_in6 *)cur_ai->ai_addr; if (!sockaddr_v6->sin6_port){ sockaddr_v6->sin6_port = htons(port); } } #endif fd = ff_socket(cur_ai->ai_family, cur_ai->ai_socktype, cur_ai->ai_protocol); if (fd < 0) { ret = ff_neterrno(); goto fail; } /* Set the socket's send or receive buffer sizes, if specified. If unspecified or setting fails, system default is used. */ if (s->recv_buffer_size > 0) { setsockopt (fd, SOL_SOCKET, SO_RCVBUF, &s->recv_buffer_size, sizeof (s->recv_buffer_size)); } if (s->send_buffer_size > 0) { setsockopt (fd, SOL_SOCKET, SO_SNDBUF, &s->send_buffer_size, sizeof (s->send_buffer_size)); } if (s->listen == 2) { // multi-client if ((ret = ff_listen(fd, cur_ai->ai_addr, cur_ai->ai_addrlen)) < 0) goto fail1; } else if (s->listen == 1) { // single client if ((ret = ff_listen_bind(fd, cur_ai->ai_addr, cur_ai->ai_addrlen, s->listen_timeout, h)) < 0) goto fail1; // Socket descriptor already closed here. Safe to overwrite to client one. fd = ret; } else { ret = av_application_on_tcp_will_open(s->app_ctx); if (ret) { av_log(NULL, AV_LOG_WARNING, "terminated by application in AVAPP_CTRL_WILL_TCP_OPEN"); goto fail1; } if ((ret = ff_listen_connect(fd, cur_ai->ai_addr, cur_ai->ai_addrlen, s->open_timeout / 1000, h, !!cur_ai->ai_next)) < 0) { if (av_application_on_tcp_did_open(s->app_ctx, ret, fd, &control)) goto fail1; if (ret == AVERROR_EXIT) goto fail1; else goto fail; } else { ret = av_application_on_tcp_did_open(s->app_ctx, 0, fd, &control); if (ret) { av_log(NULL, AV_LOG_WARNING, "terminated by application in AVAPP_CTRL_DID_TCP_OPEN"); goto fail1; } else if (!dns_entry && strcmp(control.ip, hostname_bak)) { add_dns_cache_entry(hostname_bak, cur_ai, s->dns_cache_timeout); av_log(NULL, AV_LOG_INFO, "Add dns cache hostname = %s, ip = %s\n", hostname_bak , control.ip); } } } h->is_streamed = 1; s->fd = fd; if (dns_entry) { release_dns_cache_reference(hostname_bak, &dns_entry); } else { freeaddrinfo(ai); } av_log(NULL, AV_LOG_INFO, "tcp_open end"); return 0; // 省略部分代码 }
改动地方主要是hints.ai_family = AF_INET;,原来是 hints.ai_family = AF_UNSPEC;,原来设计是一个兼容IPv4和IPv6的配置,如果修改成AF_INET,那么就不会有AAAA的查询包了。如果只有IPv4的请求,就可以改成AF_INET。当然有IPv6,这里就不要动了。这么看是否有,可以通过抓包工具看。
接着分析,我们发现tcp_read函数是个阻塞式的,会非常耗时,我们又不能设置短一点中断时间,因为短了的话,造成读取不到数据,就中断,后续播放就直接失败了,这里只能让它等。不过还是优化的点时下面部分 static int tcp_read(URLContext *h, uint8_t *buf, int size) { av_log(NULL, AV_LOG_INFO, "tcp_read begin %d\n", size); TCPContext *s = h->priv_data; int ret; if (!(h->flags & AVIO_FLAG_NONBLOCK)) { ret = ff_network_wait_fd_timeout(s->fd, 0, h->rw_timeout, &h->interrupt_callback); if (ret) return ret; } ret = recv(s->fd, buf, size, 0); if (ret == 0) return AVERROR_EOF; //if (ret > 0) // av_application_did_io_tcp_read(s->app_ctx, (void*)h, ret); av_log(NULL, AV_LOG_INFO, "tcp_read end %d\n", ret); return ret < 0 ? ff_neterrno() : ret; }
我们可以把上面两行注释掉,因为在ff_network_wait_fd_timeout等回来后,数据可以放到buf中,下面av_application_did_io_tcp_read就没必要去执行了。原来每次ret>0,都会执行av_application_did_io_tcp_read这个函数。
解复用耗时
在日志中发现,数据请求到后,进行音视频分离时,首先需要匹配对应demuxer,其中ffmpeg的av_find_input_format和avformat_find_stream_info非常耗时,前者简单理解就是打开某中请求到数据,后者就是探测流的一些信息,做一些样本检测,读取一定长度的码流数据,来分析码流的基本信息,为视频中各个媒体流的 AVStream 结构体填充好相应的数据。这个函数中做了查找合适的解码器、打开解码器、读取一定的音视频帧数据、尝试解码音视频帧等工作,基本上完成了解码的整个流程。这时一个同步调用,在不清楚视频数据的格式又要做到较好的兼容性时,这个过程是比较耗时的,从而会影响到播放器首屏秒开。这两个函数调用都在ff_ffplay.c的read_thread函数中: if (ffp->iformat_name) { av_log(ffp, AV_LOG_INFO, "av_find_input_format noraml begin"); is->iformat = av_find_input_format(ffp->iformat_name); av_log(ffp, AV_LOG_INFO, "av_find_input_format normal end"); } else if (av_stristart(is->filename, "rtmp", NULL)) { av_log(ffp, AV_LOG_INFO, "av_find_input_format rtmp begin"); is->iformat = av_find_input_format("flv"); av_log(ffp, AV_LOG_INFO, "av_find_input_format rtmp end"); ic->probesize = 4096; ic->max_analyze_duration = 2000000; ic->flags |= AVFMT_FLAG_NOBUFFER; } av_log(ffp, AV_LOG_INFO, "avformat_open_input begin"); err = avformat_open_input(&ic, is->filename, is->iformat, &ffp->format_opts); av_log(ffp, AV_LOG_INFO, "avformat_open_input end"); if (err < 0) { print_error(is->filename, err); ret = -1; goto fail; } ffp_notify_msg1(ffp, FFP_MSG_OPEN_INPUT); if (scan_all_pmts_set) av_dict_set(&ffp->format_opts, "scan_all_pmts", NULL, AV_DICT_MATCH_CASE); if ((t = av_dict_get(ffp->format_opts, "", NULL, AV_DICT_IGNORE_SUFFIX))) { av_log(NULL, AV_LOG_ERROR, "Option %s not found.\n", t->key); #ifdef FFP_MERGE ret = AVERROR_OPTION_NOT_FOUND; goto fail; #endif } is->ic = ic; if (ffp->genpts) ic->flags |= AVFMT_FLAG_GENPTS; av_format_inject_global_side_data(ic); if (ffp->find_stream_info) { AVDictionary **opts = setup_find_stream_info_opts(ic, ffp->codec_opts); int orig_nb_streams = ic->nb_streams; do { if (av_stristart(is->filename, "data:", NULL) && orig_nb_streams > 0) { for (i = 0; i < orig_nb_streams; i++) { if (!ic->streams[i] || !ic->streams[i]->codecpar || ic->streams[i]->codecpar->profile == FF_PROFILE_UNKNOWN) { break; } } if (i == orig_nb_streams) { break; } } ic->probesize=100*1024; ic->max_analyze_duration=5*AV_TIME_BASE; ic->fps_probe_size=0; av_log(ffp, AV_LOG_INFO, "avformat_find_stream_info begin"); err = avformat_find_stream_info(ic, opts); av_log(ffp, AV_LOG_INFO, "avformat_find_stream_info end"); } while(0); ffp_notify_msg1(ffp, FFP_MSG_FIND_STREAM_INFO);
最终改的如上,主要是对rtmp增加了,指定format为‘flv’,以及样本大小。 同时在外部可以通过设置 probesize 和 analyzeduration 两个参数来控制该函数读取的数据量大小和分析时长为比较小的值来降低 avformat_find_stream_info的耗时,从而优化播放器首屏秒开。但是,需要注意的是这两个参数设置过小时,可能会造成预读数据不足,无法解析出码流信息,从而导致播放失败、无音频或无视频的情况。所以,在服务端对视频格式进行标准化转码,从而确定视频格式,进而再去推算 avformat_find_stream_info分析码流信息所兼容的最小的probesize和 analyzeduration,就能在保证播放成功率的情况下最大限度地区优化首屏秒开。
在 FFmpeg 中的 utils.c 文件中的函数实现中有一行代码是 int fps_analyze_framecount = 20;,这行代码的大概用处是,如果外部没有额外设置这个值,那么 avformat_find_stream_info 需要获取至少 20 帧视频数据,这对于首屏来说耗时就比较长了,一般都要 1s 左右。而且直播还有实时性的需求,所以没必要至少取 20 帧。将这个值初始化为2,看看效果。 /* check if one codec still needs to be handled */ for (i = 0; i < ic->nb_streams; i++) { int fps_analyze_framecount = 2; st = ic->streams[i]; if (!has_codec_parameters(st, NULL)) break; if (ic->metadata) { AVDictionaryEntry *t = av_dict_get(ic->metadata, "skip-calc-frame-rate", NULL, AV_DICT_MATCH_CASE); if (t) { int fps_flag = (int) strtol(t->value, NULL, 10); if (!st->r_frame_rate.num && st->avg_frame_rate.num > 0 && st->avg_frame_rate.den > 0 && fps_flag > 0) { int avg_fps = st->avg_frame_rate.num / st->avg_frame_rate.den; if (avg_fps > 0 && avg_fps <= 120) { st->r_frame_rate.num = st->avg_frame_rate.num; st->r_frame_rate.den = st->avg_frame_rate.den; } } } }
这样,avformat_find_stream_info 的耗时就可以缩减到 100ms 以内。
最后就是解码耗时和渲染出图耗时,这块优化空间很少,大头都在前面。
有人开始抛出问题了,你这个起播快是快,但是后面网络不好,卡顿怎么办?直播中会引起卡顿,主要是网络有抖动的时候,没有足够的数据来播放,ijkplayer会激发其缓冲机制,主要是有几个宏控制 DEFAULT_FIRST_HIGH_WATER_MARK_IN_MS:网络差时首次去唤醒read_thread函数去读取数据。 DEFAULT_NEXT_HIGH_WATER_MARK_IN_MS:第二次去唤醒read_thread函数去读取数据。 DEFAULT_LAST_HIGH_WATER_MARK_IN_MS这个宏的意思是最后的机会去唤醒read_thread函数去读取数据。
可以设置DEFAULT_LAST_HIGH_WATER_MARK_IN_MS为1 * 1000,也即缓冲1秒后开始通知缓冲完成去读取数据,默认是5秒,如果过大,会让用户等太久,那么每次读取的bytes也可以少些。可以设置DEFAULT_HIGH_WATER_MARK_IN_BYTES小一些,设置为30 * 1024,默认是256 * 1024。把BUFFERING_CHECK_PER_MILLISECONDS设置为50,默认是500 #define DEFAULT_HIGH_WATER_MARK_IN_BYTES (30 * 1024) #define DEFAULT_FIRST_HIGH_WATER_MARK_IN_MS (100) #define DEFAULT_NEXT_HIGH_WATER_MARK_IN_MS (1 * 1000) #define DEFAULT_LAST_HIGH_WATER_MARK_IN_MS (1 * 1000) #define BUFFERING_CHECK_PER_BYTES (512) #define BUFFERING_CHECK_PER_MILLISECONDS (50)
可以看下这些宏使用的地方 inline static void ffp_reset_demux_cache_control(FFDemuxCacheControl *dcc) { dcc->min_frames = DEFAULT_MIN_FRAMES; dcc->max_buffer_size = MAX_QUEUE_SIZE; dcc->high_water_mark_in_bytes = DEFAULT_HIGH_WATER_MARK_IN_BYTES; dcc->first_high_water_mark_in_ms = DEFAULT_FIRST_HIGH_WATER_MARK_IN_MS; dcc->next_high_water_mark_in_ms = DEFAULT_NEXT_HIGH_WATER_MARK_IN_MS; dcc->last_high_water_mark_in_ms = DEFAULT_LAST_HIGH_WATER_MARK_IN_MS; dcc->current_high_water_mark_in_ms = DEFAULT_FIRST_HIGH_WATER_MARK_IN_MS; }
最后优化的点,是设置一些参数值,也能优化一部分,实际上很多直播用软件用低分辨率240,甚至360,来达到秒开,可以可以作为一个减少耗时点来展开的,因为分辨率越低,数据量越少,首开越快。 mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "opensles", 0); mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "framedrop", 1); mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "start-on-prepared", 1); mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "http-detect-range-support", 0); mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "fflags", "nobuffer"); mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "flush_packets", 1); mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "max_delay", 0); mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_CODEC, "skip_loop_filter", 48); mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "packet-buffering", 0); mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "max-buffer-size", 4 * 1024); mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "min-frames", 50); mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "probsize", "1024"); mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "analyzeduration", "100"); mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "dns_cache_clear", 1); //静音 //mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, "an", 1); //重连模式,如果中途服务器断开了连接,让它重新连接 mediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, "reconnect", 1);
以上完了后,就可以看下测试数据,分辨率在540p以下基本秒开,在4G网络下测试:
1、河北卫视直播源,测试10组,平均下来300ms。一组数据386ms,如下:
11-17 14:17:46.659 9896 10147 D IJKMEDIA: IjkMediaPlayer_native_setup 11-17 14:17:46.663 9896 10147 V IJKMEDIA: setDataSource: path http://weblive.hebtv.com/live/hbws_bq/index.m3u8 11-17 14:17:46.666 9896 10177 I FFMPEG : [FFPlayer @ 0xe070d400] avformat_open_input begin 11-17 14:17:46.841 9896 10177 I FFMPEG : [FFPlayer @ 0xe070d400] avformat_open_input end 11-17 14:17:46.841 9896 10177 I FFMPEG : [FFPlayer @ 0xe070d400] avformat_find_stream_info begin 11-17 14:17:46.894 9896 10177 I FFMPEG : [FFPlayer @ 0xe070d400] avformat_find_stream_info end 11-17 14:17:47.045 9896 10191 D IJKMEDIA: Video: first frame decoded 11-17 14:17:47.046 9896 10175 D IJKMEDIA: FFP_MSG_VIDEO_DECODED_START:
2、映客直播秀场源,测试10组,平均下来400ms。一组数据418ms,如下:
11-17 14:21:32.908 11464 11788 D IJKMEDIA: IjkMediaPlayer_native_setup 11-17 14:21:32.952 11464 11788 V IJKMEDIA: setDataSource: path http://14.215.100.45/hw.pull.inke.cn/live/1542433669916866_0_ud.flv?ikDnsOp=1001&ikHost=hw&ikOp=0&codecInfo=8192&ikLog=1&ikSyncBeta=1&dpSrc=6&push_host=trans.push.cls.inke.cn&ikMinBuf=2900&ikMaxBuf=3600&ikSlowRate=0.9&ikFastRate=1.1 11-17 14:21:32.996 11464 11818 I FFMPEG : [FFPlayer @ 0xc2575c00] avformat_open_input begin 11-17 14:21:33.161 11464 11818 I FFMPEG : [FFPlayer @ 0xc2575c00] avformat_open_input end 11-17 14:21:33.326 11464 11829 D IJKMEDIA: Video: first frame decoded
3、熊猫直播游戏直播源,测试10组,平均下来350ms。一组数据373ms,如下:
11-17 14:29:17.615 15801 16053 D IJKMEDIA: IjkMediaPlayer_native_setup 11-17 14:29:17.645 15801 16053 V IJKMEDIA: setDataSource: path http://flv-live-qn.xingxiu.panda.tv/panda-xingxiu/dc7eb0c2e78c96646591aae3a20b0686.flv 11-17 14:29:17.649 15801 16079 I FFMPEG : [FFPlayer @ 0xeb5ef000] avformat_open_input begin 11-17 14:29:17.731 15801 16079 I FFMPEG : [FFPlayer @ 0xeb5ef000] avformat_open_input end 11-17 14:29:17.988 15801 16090 D IJKMEDIA: Video: first frame decoded
欢迎关注我的微信公众号「码农突围」,分享Python、Java、大数据、机器学习、人工智能等技术,关注码农技术提升•职场突围•思维跃迁,20万+码农成长充电第一站,陪有梦想的你一起成长。
多媒体
2020-03-17 22:58:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
在这个火热的七月里,芯片行业老大哥英特尔动作不断。先是发布了新一代的至强可扩展处理器,紧接着对外公布了基于Apache Spark的分布式开源深度学习框架BigDL的新版本,并计划于近期正式发布。不久前,笔者曾在北京采访了英特尔公司软件与服务事业部副总裁,系统技术和优化部门大数据技术总监马子雅女士,就深度学习四大痛点与BigDL解决之道进行了交流与沟通。近期,笔者再次跟随英特尔转战上海,来到位上海紫竹科学园区的英特尔亚太研发有限公司,对英特尔公司软件与服务事业部副总裁、系统技术及优化部门总经理Michael Greene先生和英特尔大数据***架构师、资深***工程师戴金权先生进行了采访。
多媒体
2020-03-15 13:02:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
我在Wordpress里点击安装插件之后,wordpress插件上传失败原因和处理方法有哪些呢?
遇到如下错误消息:无法建立目录wp-content/uploads:
查看文件夹wp-admi的权限,发现除了root用户外,其他用户只有只读权限:
解决方案: chmod 777 ./wp-content
他用户对wp-content也拥有了写权限,问题解决。wordpress插件上传失败原因和处理方法就得到了改善了。
多媒体
2020-03-15 12:55:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
之前我们介绍了在线短视频秒播优化的方方面面,从服务器,cdn部署接入,数据连接/获取,客户端缓存,出帧策略,到视频文件I帧位置等。
今天从视频文件格式的另外一个角度介绍,MP4文件的box排列顺序是如何影响,在线短视频的播放以及秒播优化的。 MP4文件中的所有数据都装在box中 (iso-14496-12/14) (QuickTime中为atom)即mp4是由若干个box组成的
先简单介绍几个重要的box,以便诸位在后续学习时心中有数: 1、 ftyp box,在文件的开始位置,描述的文件的版本、兼容协议等; 2、 moov box,这个box中不包含具体媒体数据,但包含本文件中所有媒体数据的宏观描述信息,moov box下有mvhd和trak box。mvhd中记录了创建时间、修改时间、时间度量标尺、可播放时长等信息。trak中的一系列子box描述了每个媒体轨道的具体信息。 3、 moof box,这个box是视频分片的描述信息。并不是MP4文件必须的部分,但在我们常见的可在线播放的MP4格式文件中(例如Silverlight Smooth Streaming中的ismv文件)确是重中之重。 4、 mdat box,实际媒体数据。我们最终解码播放的数据都在这里面。 5、 mfra box,一般在文件末尾,媒体的索引文件,可通过查询直接定位所需时间点的媒体数据。
MP4文件的生成与解析,播放
两个重要的box,moov and mdat 1.生成:先写入mdat后写入moov,因此绝大多数工具都会把moov数据放到mdat后边,比如android的mp4writer,ffmpeg等工具 2.解析:解析播放的时候,先读取moov,才能解析mdat
播放影响
1.本地播放,没有影响,播放软件可以先seek到末尾,读取moov 2.在线播放 (1).需要http服务器支持seek (2).服务器不支持seek,是个非常不友好的方案,要先把数据下载完成才能播放(无论下载到哪里,新服务器,本地内存或存储) (3).(1)和(2)多多少少会引入延时,尤其(2),影响秒开
市面上短视频mp4 box排列 抖音,火山等小视频moov排在前边,不用seek; 快手,360等moov排在后边需要seek;
在线短视频MP4 moov box排在后边的解决方案 1.修改文件把moov box排在前面,在MP4在设备上生成的时候或传到服务器上后进行,这个方法一劳永逸,还能提升秒开的速度 工具: ffmpeg option faststart qt-faststart
原创作者:Walker.Xu,原文链接: https://segmentfault.com/a/1190000014405913
欢迎关注我的微信公众号「码农突围」,分享Python、Java、大数据、机器学习、人工智能等技术,关注码农技术提升•职场突围•思维跃迁,20万+码农成长充电第一站,陪有梦想的你一起成长。
多媒体
2020-03-13 22:28:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
(1)首先要启动win7系统中的iis7远程桌面启动方法:直接解压打开。注意:没有设置之前默认登录为“此账户”,网络服务。此时不用修改。若不是的情况下,则可以点击浏览->高级->立即查找->在下面选择Network service即可,然后设置相应的密码。
(2)在“我的电脑”右击属性,然后点击“远程设置”,在弹出的系统属性框中,点击远程,选择“允许运行任意版本远程桌面的计算机连接(较不安全)(L)”选项。
(3)即可在远程计算机的远程登录框中输入登录主机的IP地址,然后根据用户名和密码登录到远程计算机
具体方法如下:
iis7 远程桌面连接工具,又叫做 iis7 远程桌面管理软件,是一款绿色小巧,功能实用的远程桌面管理工具,其界面简洁,操作便捷,能够同时远程操作多台服务器,并且多台服务器间可以自由切换,适用于网站管理人员使用。
查看地址: iis7 远程桌面管理工具下载
那么这个软件如何使用呢?
首先下载解压软件,因为本软件是绿色软件,免安装,所以解压后直接双击“ IIS7 远程桌面管理 .exe ”就可以看到程序的主界面了:


然后在界面的中间偏右的部分会看到“添加机器”,单击打开之后会看到:


在这里添加服务器的详细信息,
在这里一定要填写的是【服务器 IP 和端口】、【服务器账号】、【服务器密码】下图所标记的地方:

【注意】
1、 输入服务器端口后用冒号分隔再填写端口号(一般默认为 3389 );
2、 服务器账号一般默认为 administrator ;
3、 服务器密码就是在购买服务器时所给的密码或者自己设置的密码。
其他信息是为了方便大批量管理服务器信息的时候添加的分组信息,根据个人情况和喜好做分类。
添加完毕核对无误后就可以点击右下角的添加,就可以看到添加的服务器信息,双击就可以打开啦!


如果您要批量打开多台服务器,点击【全选】之后,再点击【打开远程】即可:

打开桌面的效果图:

以上就是iis7远程桌面的远程连接服务器方法啦,以后还会有更多功能,敬请期待吧!
多媒体
2020-03-13 13:21:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
如何远程连接服务器(Remote Access Service,RAS),就是把在互联网中的计算机和在局域网中的远程访问服务器相(RAS)连接,从而在RAS和RAC(远程访问客户机,就是在互联网中的某计算机)建立一个虚拟专用线路来直接接入到RAS,连接上RAS后,就可以访问处于RAS的局域网从而获取在局域网中的资源。
如何远程连接服务器?
远程访问服务允许客户机通过拨号连接或虚拟专用连接登录网络。远程客户机一旦得到RAS服务器的确认,就可以访问网络服务,就好像客户机直接连接在局域网上一样。
远程连接服务器,就是我们可以在自己电脑上通过工具远程控制互联网上的服务器(可以理解为另一台计算机)通过远程桌面功能我们可以实时的操作这台计算机的技术,在上面安装软件,运行程序,所有的一切都像是直接在自己计算机上操作一样。
iis7 远程桌面连接工具,又叫做 iis7 远程桌面管理软件,是一款绿色小巧,功能实用的远程桌面管理工具,其界面简洁,操作便捷,能够同时远程操作多台服务器,并且多台服务器间可以自由切换,适用于网站管理人员使用。

查看地址: iis7 远程桌面管理工具下载
那么这个软件如何使用呢?
首先下载解压软件,因为本软件是绿色软件,免安装,所以解压后直接双击“ IIS7 远程桌面管理 .exe ”就可以看到程序的主界面了:


然后在界面的中间偏右的部分会看到“添加机器”,单击打开之后会看到:


在这里添加服务器的详细信息,
在这里一定要填写的是【服务器 IP 和端口】、【服务器账号】、【服务器密码】下图所标记的地方:

【注意】
1、 输入服务器端口后用冒号分隔再填写端口号(一般默认为 3389 );
2、 服务器账号一般默认为 administrator ;
3、 服务器密码就是在购买服务器时所给的密码或者自己设置的密码。
其他信息是为了方便大批量管理服务器信息的时候添加的分组信息,根据个人情况和喜好做分类。
添加完毕核对无误后就可以点击右下角的添加,就可以看到添加的服务器信息,双击就可以打开啦!


如果您要批量打开多台服务器,点击【全选】之后,再点击【打开远程】即可:

打开桌面的效果图:

以上就是iis7远程桌面的远程连接服务器方法啦,以后还会有更多功能,敬请期待吧!
多媒体
2020-03-13 09:08:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
不少朋友对短视频,上下滑动播放视频效果比较比较感兴趣,今天看看这个案例。
1、效果图:

讲下大概思路,使用Recycleview配合自定义LinearLayoutManager来实现这个功能,这里着重说下自定义LinearLayoutManager的实现可以看到每当下一个item滑入屏幕时,上面的item会继续播放视频,而滑入的item只有当全部进入屏幕才会播放,而且当手指抬起时,当前item会根据滑动的距离相应的自动滑入滑出,针对这种情形,就会想到使用SnapHelper
RecyclerView在24.2.0版本中新增了SnapHelper这个辅助类,用于辅助RecyclerView在滚动结束时将Item对齐到某个位置。特别是列表横向滑动时,很多时候不会让列表滑到任意位置,而是会有一定的规则限制,这时候就可以通过SnapHelper来定义对齐规则了。
SnapHelper是一个抽象类,官方提供了一个LinearSnapHelper的子类,可以让RecyclerView滚动停止时相应的Item停留中间位置。25.1.0版本中官方又提供了一个PagerSnapHelper的子类,可以使RecyclerView像ViewPager一样的效果,一次只能滑一页,而且居中显示,也就是说使用SnapHelper可以帮助RecyclerView滑动完成后进行对齐操作,让item的侧边对齐或者居中对齐,这样实现上下滑动进行视频切换。这里有SnapHelper的详解
2、正式撸代码:
1.首先定义一个接口,用来执行item的相关操作 public interface OnViewPagerListener {     /*初始化完成*/     void onInitComplete();     /*释放的监听*/     void onPageRelease(boolean isNext, int position);     /*选中的监听以及判断是否滑动到底部*/     void onPageSelected(int position, boolean isBottom); }
2.继承LinearLayoutManager ,对滑入滑出的item回调1中接口里面的方法 import android.content.Context; import android.support.annotation.NonNull; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.PagerSnapHelper; import android.support.v7.widget.RecyclerView; import android.view.View; public class MyLayoutManager extends LinearLayoutManager implements RecyclerView.OnChildAttachStateChangeListener {     private int mDrift;//位移,用来判断移动方向     private PagerSnapHelper mPagerSnapHelper;     private OnViewPagerListener mOnViewPagerListener;     public MyLayoutManager(Context context) {         super(context);     }     public MyLayoutManager(Context context, int orientation, boolean reverseLayout) {         super(context, orientation, reverseLayout);         mPagerSnapHelper = new PagerSnapHelper();     }     @Override     public void onAttachedToWindow(RecyclerView view) {         view.addOnChildAttachStateChangeListener(this);         mPagerSnapHelper.attachToRecyclerView(view);         super.onAttachedToWindow(view);     } //当Item添加进来了  调用这个方法     //     @Override     public void onChildViewAttachedToWindow(@NonNull View view) { //        播放视频操作 即将要播放的是上一个视频 还是下一个视频         int position = getPosition(view);         if (0 == position) {             if (mOnViewPagerListener != null) {                 mOnViewPagerListener.onPageSelected(getPosition(view), false);             }         }     }     public void setOnViewPagerListener(OnViewPagerListener mOnViewPagerListener) {         this.mOnViewPagerListener = mOnViewPagerListener;     }     @Override     public void onScrollStateChanged(int state) {         switch (state) {             case RecyclerView.SCROLL\_STATE\_IDLE:                 View view = mPagerSnapHelper.findSnapView(this);                 int position = getPosition(view);                 if (mOnViewPagerListener != null) {                     mOnViewPagerListener.onPageSelected(position, position == getItemCount() - 1);                 } //                postion  ---回调 ----》播放                 break;         }         super.onScrollStateChanged(state);     }     @Override     public void onChildViewDetachedFromWindow(@NonNull View view) { //暂停播放操作         if (mDrift >= 0) {             if (mOnViewPagerListener != null)                 mOnViewPagerListener.onPageRelease(true, getPosition(view));         } else {             if (mOnViewPagerListener != null)                 mOnViewPagerListener.onPageRelease(false, getPosition(view));         }     }     @Override     public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {         this.mDrift = dy;         return super.scrollVerticallyBy(dy, recycler, state);     }     @Override     public boolean canScrollVertically() {         return true;     } }
3.接下来就是正常使用recycleview了 配合原生VideoView 播放视频,切换时先用一张截图盖住视频,视频渲染成功再隐藏截图,感觉上是无缝切换(这里是原生播放器初始加载视频会黑屏,如果用更高级的播放器可能不会有这个问题) import android.annotation.TargetApi; import android.content.Context; import android.media.MediaPlayer; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.OrientationHelper; import android.support.v7.widget.RecyclerView; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.RelativeLayout; import android.widget.VideoView; public class MainActivity extends AppCompatActivity {     private static final String TAG = "douyin";     private RecyclerView mRecyclerView;     private MyAdapter mAdapter;     MyLayoutManager2 myLayoutManager;     @Override     protected void onCreate(Bundle savedInstanceState) {         super.onCreate(savedInstanceState);         setContentView(R.layout.activity_main);         initView();         initListener();     }     private void initView() {         mRecyclerView = findViewById(R.id.recycler);         myLayoutManager = new MyLayoutManager2(this, OrientationHelper.VERTICAL, false);         mAdapter = new MyAdapter(this);         mRecyclerView.setLayoutManager(myLayoutManager);         mRecyclerView.setAdapter(mAdapter);     }     private void initListener() {         myLayoutManager.setOnViewPagerListener(new OnViewPagerListener() {             @Override             public void onInitComplete() {             }             @Override             public void onPageRelease(boolean isNext, int position) {                 Log.e(TAG, "释放位置:" + position + " 下一页:" + isNext);                 int index = 0;                 if (isNext) {                     index = 0;                 } else {                     index = 1;                 }                 releaseVideo(index);             }             @Override             public void onPageSelected(int position, boolean bottom) {                 Log.e(TAG, "选择位置:" + position + " 下一页:" + bottom);                 playVideo(0);             }         });     }     class MyAdapter extends RecyclerView.Adapter {         private int\[\] imgs = {R.mipmap.img\_video\_1, R.mipmap.img\_video\_2, R.mipmap.img\_video\_3, R.mipmap.img\_video\_4, R.mipmap.img\_video\_5, R.mipmap.img\_video\_6, R.mipmap.img\_video\_7, R.mipmap.img\_video\_8};         private int\[\] videos = {R.raw.video\_1, R.raw.video\_2, R.raw.video\_3, R.raw.video\_4, R.raw.video\_5, R.raw.video\_6, R.raw.video\_7, R.raw.video\_8};         private int index = 0;         private Context mContext;         public MyAdapter(Context context) {             this.mContext = context;         }         @Override         public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {             View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item\_view\_pager, parent, false);             return new ViewHolder(view);         }         @Override         public void onBindViewHolder(ViewHolder holder, int position) {             holder.img_thumb.setImageResource(imgs\[index\]);             holder.videoView.setVideoURI(Uri.parse("android.resource://" + getPackageName() + "/" + videos\[index\]));             index++;             if (index >= 7) {                 index = 0;             }         }         @Override         public int getItemCount() {             return 88;         }         public class ViewHolder extends RecyclerView.ViewHolder {             ImageView img_thumb;             VideoView videoView;             ImageView img_play;             RelativeLayout rootView;             public ViewHolder(View itemView) {                 super(itemView);                 img\_thumb = itemView.findViewById(R.id.img\_thumb);                 videoView = itemView.findViewById(R.id.video_view);                 img\_play = itemView.findViewById(R.id.img\_play);                 rootView = itemView.findViewById(R.id.root_view);             }         }     }     private void releaseVideo(int index) {         View itemView = mRecyclerView.getChildAt(index);         final VideoView videoView = itemView.findViewById(R.id.video_view);         final ImageView imgThumb = itemView.findViewById(R.id.img_thumb);         final ImageView imgPlay = itemView.findViewById(R.id.img_play);         videoView.stopPlayback();         imgThumb.animate().alpha(1).start();         imgPlay.animate().alpha(0f).start();     }     @TargetApi(Build.VERSION\_CODES.JELLY\_BEAN_MR1)     private void playVideo(int position) {         View itemView = mRecyclerView.getChildAt(position);         final FullWindowVideoView videoView = itemView.findViewById(R.id.video_view);         final ImageView imgPlay = itemView.findViewById(R.id.img_play);         final ImageView imgThumb = itemView.findViewById(R.id.img_thumb);         final RelativeLayout rootView = itemView.findViewById(R.id.root_view);         final MediaPlayer\[\] mediaPlayer = new MediaPlayer\[1\];         videoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {             @Override             public void onPrepared(MediaPlayer mp) {             }         });         videoView.setOnInfoListener(new MediaPlayer.OnInfoListener() {             @Override             public boolean onInfo(MediaPlayer mp, int what, int extra) {                 mediaPlayer\[0\] = mp;                 mp.setLooping(true);                 imgThumb.animate().alpha(0).setDuration(200).start();                 return false;             }         });         videoView.start();         imgPlay.setOnClickListener(new View.OnClickListener() {             boolean isPlaying = true;             @Override             public void onClick(View v) {                 if (videoView.isPlaying()) {                     imgPlay.animate().alpha(0.7f).start();                     videoView.pause();                     isPlaying = false;                 } else {                     imgPlay.animate().alpha(0f).start();                     videoView.start();                     isPlaying = true;                 }             }         });     } }
原创作者:庞哈哈哈12138,原文: https://www.jianshu.com/p/3a043cd4eb1f
欢迎关注我的微信公众号「码农突围」,分享Python、Java、大数据、机器学习、人工智能等技术,关注码农技术提升•职场突围•思维跃迁,20万+码农成长充电第一站,陪有梦想的你一起成长。
多媒体
2020-03-10 21:05:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
在三维图形开发中因为性能优化的原因,经常要把在视椎体外的物体剔除掉,做资源释放。检测物体是否在视椎体内有两种方式:
一. 投影矩阵法,步骤如下:
(1): result = (project矩阵 * camera矩阵 * 顶点世界坐标)
(2): 然后对result做透视除法,result.x = result.x/result.w, result.y = result.y/result.w, result.z = result.z/result.w
(3): 如果result的x,y,z都在-1和1的范围内,那就是可见,否则就是不可见。
二. 视椎体6个平面和点检测,步骤如下:
(1): 得到视椎体的6个平面,这6个平面在归一化空间里面分别为 (-1, 0, 0, 1), (+1, 0, 0, 1), (0, -1, 0, 1), (0, +1, 0, 1), (0, 0, -1, 1), (0, 0, +1, 1),对它们分别乘上(view * project)的逆矩阵就得到在世界空间中的平面了。
(2): 一个平面的一般方程式可以表示为Ax+By+Cz+D=0,这里的A,B,C分别为平面的法线,D为平面沿着该法线向量到原点的距离。如果任意一个点(x, y, z)在这个平面上,或者在平面的上方,可以把这个点带入平面的一般方程式,得到Ax+By+Cz+D >= 0;如果在平面的下方,会得到Ax+By+Cz+D < 0。可以用这个理论把一个点带入到6个平面方程中,如果都是大于等于0,那么这个点就在视椎体内。
多媒体
2020-03-09 21:44:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
问题
- 主流程上的区别
- 缓冲区的设计
- 内存管理的逻辑
- 音视频播放方式
- 音视频同步
- seek的问题:缓冲区flush、播放时间显示、k帧间距大时定位不准问题…
- stop时怎么释放资源,是否切换到副线程?
- 网络不好时的处理,如获取frame速度慢于消耗速度时,如果不暂停,会一致卡顿,是否会主动暂停?
- VTB的解码和ffmpeg的解码怎么统一的?架构上怎么设计的?
数据流向
主流程更详细看ijkPlayer主流程分析
音频
- av_read_frame
- packet_queue_put
- audio_thread+decoder_decode_frame+packet_queue_get_or_buffering
- frame_queue_peek_writable+frame_queue_push
- audio_decode_frame+frame_queue_peek_readable,数据到is->audio_buf
sdl_audio_callback,数据导入到参数stream里。这个函数是上层的音频播放库的buffer填充函数,如iOS里使用audioQueue,回调函数IJKSDLAudioQueueOuptutCallback调用到这里,然后把数据传入到audioQueue.
视频
读取packet部分一样
video_thread,然后ffpipenode_run_sync里硬解码定位到videotoolbox_video_thread,然后ffp_packet_queue_get_or_buffering读取。
VTDecoderCallback解码完成回调里,SortQueuePush(ctx, newFrame);把解码后的pixelBuffer装入到一个有序的队列里。
GetVTBPicture从有序队列里把frame的封装拿出来,也就是这个有序队列只是一个临时的用来排序的工具罢了,这个思想是可以吸收的;queue_picture里,把解码的frame放入frame缓冲区
显示video_refresh+video_image_display2+[IJKSDLGLView display:]
最后的纹理生成放在了render里,对vtb的pixelBuffer,在yuv420sp_vtb_uploadTexture。使用render这个角色,渲染的部分都抽象出来了。shader在IJK_GLES2_getFragmentShader_yuv420sp
结论:主流程上没有大的差别。
缓冲区的设计
packetQueue:
1、数据结构设计
packetQueue采用两条链表,一个是保存数据的链表,一个是复用节点链表,保存没有数据的那些节点。数据链表从first_pkt到last_pkt,插入数据接到last_pkt的后面,取数据从first_pkt拿。复用链表的开头是recycle_pkt,取完数据后的空节点,放到空链表recycle_pkt的头部,然后这个空节点成为新的recycle_pkt。存数据时,也从recycle_pkt复用一个节点。
链表的节点像是包装盒,装载数据的时候放到数据链表,数据取出后回归到复用链表。
2、进出的阻塞控制
取数据的时候可能没有,那么就有几种处理:直接返回、阻塞等待。它这里的处理是阻塞等待,并且会把视频播放暂停。所以这个回答了问题8,外面看到的效果就是:网络卡的时候,会停止播放然后流畅的播放一会,然后又继续卡顿,播放和卡顿是清晰分隔的。
进数据的时候并没有做阻塞控制,为什么数据不会无限扩大?
是有阻塞的,但阻塞不是在packetQueue里面,而是在readFrame函数里: if (ffp->infinite\_buffer<1 && !is->seek\_req &&              (is->audioq.size + is->videoq.size + is->subtitleq.size > ffp->dcc.max\_buffer\_size            || (   stream\_has\_enough\_packets(is->audio\_st, is->audio\_stream, &is->audioq, MIN\_FRAMES)                && stream\_has\_enough\_packets(is->video\_st, is->video\_stream, &is->videoq, MIN\_FRAMES)                && stream\_has\_enough\_packets(is->subtitle\_st, is->subtitle\_stream, &is->subtitleq, MIN\_FRAMES)))) {            if (!is->eof) {                ffp\_toggle\_buffering(ffp, 0);            }            /\* wait 10 ms */            SDL\_LockMutex(wait\_mutex);            SDL\_CondWaitTimeout(is->continue\_read\_thread, wait\_mutex, 10);            SDL\_UnlockMutex(wait\_mutex);            continue;        }
简化来看就是:
- infinite_buffer不是无限的缓冲
- is->audioq.size + is->videoq.size + is->subtitleq.size > ffp->dcc.max_buffer_size,使用数据大小做限制
- stream_has_enough_packets使用数据的个数做限制
因为个数设置到了50000,一般达不到,而是数据大小做了限制,在15M左右。
这里精髓的地方有两点:
- 采用了数据大小做限制,因为对于不同的视频,分辨率的问题会导致同一个packet差距巨大,而我们实际关心的其实就是内存问题。
- 暂停10ms,而不是无限暂停等待条件锁的signal。从设计上说会更简单,而且可以避免频繁的wait+signal。这个问题还需仔细思考,但直觉上觉得这样的操作非常好。
**frameQueue:**
数据使用一个简单的数组保存,可以把这个数据看成是环形的,然后也是其中一段有数据,另一段没有数据。rindex表示数据开头的index,也是读取数据的index,即read index,windex表示空数据开头的index,是写入数据的index,即write index。
也是不断循环重用,然后size表示当前数据大小,max_size表示最大的槽位数,写入的时候如果size满了,就会阻塞等待;读取的时候size为空,也会阻塞等待。
有个奇怪的东西是rindex_shown,读取的时候不是读的rindex位置的数据,而是rindex+rindex_shown,需要结合后面的使用情况再看这个的作用。后面再看。
还有serial没有明白什么意思
结论:缓冲区的设计和我的完全不同,但都使用重用的概念,而且节点都是包装盒,数据包装在节点里面。性能上不好比较,但我的设计更完善,frame和packet使用统一设计,还包含了排序功能。
内存管理
**packet的管理**
从av_read_frame得到初始值,这个时候引用数为1,packet是使用一个临时变量去接的,也就是栈内存。然后加入队列时,pkt1->pkt = *pkt;使用值拷贝的方式把packet存入,这样缓冲区的数据和外面的临时变量就分离了。
packet_queue_get_or_buffering把packet取出来,同样使用值复制的方式。
最后使用av_packet_unref把packet关联的buf释放掉,而临时变量的packet可以继续使用。
需要注意的一点是:avcodec_send_packet返回EAGAIN表示当前还无法接受新的packet,还有frame没有取出来,所以有了: d->packet_pending = 1; av\_packet\_move_ref(&d->pkt, &pkt);
把这个packet存到d->pkt,在下一个循环里,先取frame,再把packet接回来,接着上面的操作: if (d->packet_pending) {     av\_packet\_move_ref(&pkt, &d->pkt);     d->packet_pending = 0; }
可能是存在B帧的时候会这样,因为B帧需要依赖后面的帧,所以不会解码出来,等到后面的帧传入后,就会有多个帧需要读取。这时解码器应该就不接受新的packet。但ijkplayer这里的代码似乎不会出现这样的情况,因为读取frame不是一次一个,而是一次性读到报EAGAIN错误未知。待考察。
另,av_packet_move_ref这个函数就是完全的只复制,source的值完全的搬到destination,并且把source重置掉。其实就是搬了个位置,buf的引用数不改变。
视频frame的内存管理
在ffplay_video_thread里,frame是一个对内存,使用get_video_frame从解码器读取到frame。这时frame的引用为1过程中出错,使用av_frame_unref释放frame的buf的内存,但frame本身还可以继续使用。不出错,也会调用av_frame_unref,这样保证每个读取的frame都会unref,这个unref跟初始化是对应的。使用引用指数来管理内存,重要的原则就是一一对应。
因为这里只是拿到frame,然后存入缓冲区,还没有到使用的时候,如果buf被释放了,那么到播放的时候,数据就丢失了,所以是怎么处理的呢?存入缓冲区在queue_picture里,再到SDL_VoutFillFrameYUVOverlay,这个函数会到上层,根据解码器不同做不同处理,以ijksdl_vout_overlay_ffmpeg.c的func_fill_frame为例。
有两种处理:
一种是overlay和frame共享内存,就显示的直接使用frame的内存,格式是YUV420p的就是这样,因为OpenGL可以直接显示这种颜色空间的图像。这种就只需要对frame加一个引用,保证不会被释放掉就好了。关键就是这句:av_frame_ref(opaque->linked_frame, frame);
另一种是不共享,因为要转格式,另建一个frame,即这里的opaque->managed_frame,然后转格式。数据到了新地方,原frame也就没用了。不做ref操作,它自然的就会释放了。
音频frame的处理
在audio_thread里,不断通过decoder_decode_frame获取到新的frame。和视频一样,这里的frame也是对内存,读到解码后的frame后,引用为1。音频的格式转换放在了播放阶段,所以这里只是单纯的把frame存入:av_frame_move_ref(af->frame, frame);。做了一个复制,把读取的frame搬运到缓冲区里了。在frame的缓冲区取数据的时候,frame_queue_next里包含了av_frame_unref把frame释放。这个视频也是一样。有一个问题是,上层播放器的读取音频数据的时候,frame必须是活的,因为如果音频不转换格式,是直接读取了frame里的数据。所以也就是需要在填充播放器数据结束后,才可以释放frame。unref是在frame_queue_next,而这个函数是在下一次读取frame的时候才发生,下一次读取frame又是在当前的数据读完后,所以读完了数据后,才会释放frame,这样就没错了。 //数据读完才会去拉取下一个frame if (is->audio\_buf\_index >= is->audio\_buf\_size) {     audio\_size = audio\_decode_frame(ffp);
音视频的播放方式
- 音频播放使用AudioQueue:
- 构建AudioQueue:AudioQueueNewOutput
- 开始AudioQueueStart,暂停AudioQueuePause,结束AudioQueueStop
- 在回调函数IJKSDLAudioQueueOuptutCallback里,调用下层的填充函数来填充AudioQueue的buffer。
- 使用AudioQueueEnqueueBuffer把装配完的AudioQueue Buffer入队,进入播放。
上面这些都是AudioQueue的标准操作,特别的是构建AudioStreamBasicDescription的时候,也就是指定音频播放的格式。格式是由音频源的格式决定的,在IJKSDLGetAudioStreamBasicDescriptionFromSpec里看,除了格式固定为pcm之外,其他的都是从底层给的格式复制过来。这样就有了很大的自由,音频源只需要解码成pcm就可以了。
而底层的格式是在audio_open里决定的,逻辑是:
根据源文件,构建一个期望的格式wanted_spec,然后把这个期望的格式提供给上层,最后把上层的实际格式拿到作为结果返回。一个类似沟通的操作,这种思维很值得借鉴
如果上传不接受这种格式,返回错误,底层修改channel数、采样率然后再继续沟通。
但是样本格式是固定为s16,即signed integer 16,有符号的int类型,位深为16比特的格式。位深指每个样本存储的内存大小,16个比特,加上有符号,所以范围是[-2^15, 215-1],215为32768,变化性足够了。
因为都是pcm,是不压缩的音频,所以决定性的因素就只有:采样率、通道数和样本格式。样本格式固定s16,和上层沟通就是决定采样率和通道数。这里是一个很好的分层架构的例子,底层通用,上层根据平台各有不同。
视频的播放:
播放都是使用OpenGL ES,使用IJKSDLGLView,重写了layerClass,把layer类型修改为CAEAGLLayer可以显示OpenGL ES的渲染内容。所有类型的画面都使用这个显示,有区别的地方都抽象到Render这个角色里了,相关的方法有:
- setupRenderer 构建一个render
- IJK_GLES2_Renderer_renderOverlay 绘制overlay。
render的构建包括:
- 使用不同的fragmnt shader和共通的vertex shader构建program
- 提供mvp矩阵
- 设置顶点和纹理坐标数据
render的绘制包括:
- func_uploadTexture定位到不同的render,执行不同的纹理上传操作
- 绘制图形使用glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);,使用了图元GL_TRIANGLE_STRIP而不是GL_TRIANGLE,可以节省顶点。
提供纹理的方法也是重点,区别在于颜色空间以及元素的排列方式:
rgb类型的提供了3种:565、888和8888。rgb类型的元素都是混合在一起的,也就是只有一个层(plane),565指是rgb3个元素分别占用的比特位数,同理888,8888是另外包含了alpha元素。所以每个像素565占2个字节,888占3个字节,8888占4个字节。 glTexImage2D(GL\_TEXTURE\_2D,                     0,                     GL_RGBA,                     widths\[plane\],                     heights\[plane\],                     0,                     GL_RGBA,                     GL\_UNSIGNED\_BYTE,                     pixels\[plane\]);
构建纹理的时候区别就在format跟type参数上。
- yuv420p的,这种指的是最常用的y、u、v3个元素全部开,分3层,然后数量比是4:1:1,所以u v的纹理大小高和宽都是y纹理的一半。然后因为每个分量各自一个纹理,所以每个纹理都是单通道的,使用的format为GL_LUMINANCE
- yuv420sp的,这种yuv的比例也是4:1:1,区别在于u v不是分开两层,而是混合在同一层里,分层是uuuuvvvv,混合是uvuvuvuv。所以构建两个纹理,y的纹理不变,uv的纹理使用双通道的格式GL_RG_EXT,大小也是y的1/4(高宽都为1/2)。这种在fragment shader里取值的时候会有区别: //3层的 yuv.y = (texture2D(us2\_SamplerY, vv2\_Texcoord).r - 0.5);        yuv.z = (texture2D(us2\_SamplerZ, vv2\_Texcoord).r - 0.5); //双层的 yuv.yz = (texture2D(us2\_SamplerY,  vv2\_Texcoord).rg - vec2(0.5, 0.5));
uv在同一个纹理里,texture2D直接取了rg两个分量。
- yuv444p的不是很懂,看fragment shader貌似每个像素有两个版本的yuv,然后做了一个插值。
最后是yuv420p_vtb,这个是VideoToolBox硬解出来的数据的显示,因为数据存储在CVPixelBuffer里,所以直接使用了iOS系统的纹理构建方法。
ijkplayer里的的OpenGL ES是2.0版本,如果使用3.0版本,双通道可以使用GL_LUMINANCE_ALPHA。
音视频同步
首先看音频,音频并没有做阻塞控制,上层的的播放器要需要数据都会填充,没有看到时间不到不做填充的操作。所以应该是默认了音频钟做主控制,所以音频没做处理。
1.视频显示时的时间控制
视频的控制在video_refresh里,播放函数是video_display2,进入这里代表时间到了、该播了,这是一个检测点。
有几个参数需要了解:
- is->frame_timer,这个时间代表上一帧播放的时间
- delay表示这一帧到下一帧的时间差 if (isnan(is->frame\_timer) || time < is->frame\_timer){    is->frame_timer = time; }
上一帧的播放时间在当前时间后面,说明数据错误,调整到当期时间 if (time < is->frame_timer + delay) {    \*remaining\_time = FFMIN(is->frame\_timer + delay - time, \*remaining_time);    goto display; }
is->frame_timer + delay就表示当前帧播放的时间,这个时间晚于当前时间,就表示还没到播放的时候。
这个有个坑:goto display并不是去播放了,因为display代码块里还有一个判断,判断里有个is->force_refresh。这个值默认是false,所以直接跳去display,实际的意义是啥也不干,结束这次判断。反之,如果播放时间早于当前时间,那就要马上播放了。所以更新上一帧的播放时间:is->frame_timer += delay;然后一直到后面,有个is->force_refresh = 1;,这时才是真的播放。
从上面两段就可以看出基本的流程了:
一开始当前帧播放时间没到,goto display等待下次循环,循环多次,时间不段后移,终于播放时间到了,播放当前帧,frame_timer更新为当前帧的时间。然后又重复上面的过程,去播放下一帧。然后有个问题是:为什么frame_timer的更新是加上delay,而不是直接等于当前时间?
如果直接等于当前时间,因为time>= frame_timer+delay,那么frame_timer是相对更大了一些,那么在计算下一帧时间,也就是frame_timer+delay的时候,也就会大一点。而且每一帧都会是这个情况,最后每一帧都会大那么一点,整体而言可能会有有比较大的差别。 if (delay > 0 && time - is->frame\_timer > AV\_SYNC\_THRESHOLD\_MAX){     is->frame_timer = time; }
在frame_timer比较落后的时候,直接提到当前time上,就可以直接把状态修正,之后的播放又会走上正轨。
2.同步钟以及钟时间的修正
同步钟的概念: 音频或者视频,如果把内容正确的完整的播放,某个内容和一个时间是一一对应的,当前的音频或者视频播放到哪个位置,它就有一个时间来表示,这个时间就是同步钟的时间。所以音频钟的时间表示音频播放到哪个位置,视频钟表示播放到哪个位置。
因为音频和视频是分开表现的,就可能会出现音频和视频的进度不一致,在同步钟上就表现为两个同步钟的值不同,如果让两者统一,就是音视频同步的问题。
因为有了同步钟的概念,音视频内容上的同步就可以简化为更准确的:音频钟和视频钟时间相同。
这时会有一个同步钟作为主钟,也就是其他的同步钟根据这个主钟来调整自己的时间。满了就调快、快了就调慢。
compute_target_delay里的逻辑就是这样,diff = get_clock(&is->vidclk) - get_master_clock(is);这个是视频钟和主钟的差距:
//视频落后超过临界值,缩短下一帧时间
if (diff <= -sync_threshold)    delay = FFMAX(0, delay + diff); //视频超前,且超过临界值,延长下一帧时间 else if (diff >= sync_threshold && delay > AV_SYNC_FRAMEDUP_THRESHOLD)
   delay = delay + diff;
else if (diff >= sync_threshold)
   delay = 2 * delay;
至于为什么不都是delay + diff,即为什么还有第3种1情况,我的猜测是:
延时直接加上diff,那么下一帧就直接修正了视频种和主钟的差异,但有可能这个差异已经比较大了,直接一步到位的修正导致的效果就是:画面有明显的停顿,然后声音继续播,等到同步了视频再恢复正常。而如果采用2*delay的方式,是每一次修正delay,多次逐步修正差异,可能变化上会更平滑一些。效果上就是画面和声音都是正常的,然后声音逐渐的追上声音,最后同步。至于为什么第2种情况选择一步到位的修正,第3种情况选择逐步修正,这个很难说。因为AV_SYNC_FRAMEDUP_THRESHOLD值为0.15,对应的帧率是7左右,到这个程度,视频基本都是幻灯片了,我猜想这时逐步修正也没意义了。
3.同步钟时间获取的实现
再看同步钟时间的实现:get_clock获取时间, set_clock_at更新时间。
解析一下:return c->pts_drift + time - (time - c->last_updated) * (1.0 - c->speed);,为啥这么写?
上一次显示的时候,更新了同步钟,调用set_clock_at,上次的时间为c->last_updated,则:
c->pts_drift + time = (c->pts - c->last_updated)+time;
假设距离上次的时间差time_diff = time - c->last_updated,则表达式整体可以变为:
c->pts+time_diff+(c->speed - 1)* time_diff,合并后两项变为:
c->pts+c->speed* time_diff.
我们要求得就是当前时间时的媒体内容位置,上次的位置是c->pts,而中间过去了time_diff这么多时间,媒体内容过去的时间就是:播放速度x现实时间,也就是c->speed*time_diff。举例:现实里过去10s,如果你2倍速的播放,那视频就过去了20s。所以这个表达式就很清晰了。
在set_clock_speed里同时调用了set_clock,这是为了保证从上次更新时间以来,速度是没变的,否则计算就没有意义了。到这差不多了,还有一点是在seek时候同步钟的处理,到seek问题的时候再看。
**seek的处理**
seek就是调整进度条到新的地方开始播,这个操作会打乱原本的数据流,一些播放秩序要重新建立。需要处理的问题包括:
- 缓冲区数据的释放,而且要重头到位全部释放干净
- 播放时间显示
- “加载中”的状态的维护,这个影响着用户界面的显示问题
- 剔除错误帧的问题
流程
外界seek调用到ijkmp_seek_to_l,然后发送消息ffp_notify_msg2(mp->ffplayer, FFP_REQ_SEEK, (int)msec);,消息捕获到后调用到stream_seek,然后设置seek_req为1,记录seek目标到seek_pos。在读取函数read_thread里,在is->seek_req为true时,进入seek处理,几个核心处理:
- ffp_toggle_buffering关闭解码,packet缓冲区静止
- 调用avformat_seek_file进行seek
- 成功之后用packet_queue_flush清空缓冲区,并且把flush_pkt插入进去,这时一个标记数据
- 把当前的serial记录下来
到这里值得学习的点是:
- 我在处理seek的时候,是另开一个线程调用了ffmpeg的seek方法,而这里是直接在读取线程里,这样就不用等待读取流程的结束了
- seek成功之后再flush缓冲区
因为 if (pkt == &flush_pkt)         q->serial++;
所以serial的意义就体现出来了,每次seek,serial+1,也就是serial作为一个标记,相同代表是同一次seek里的。
到decoder_decode_frame里:
- 因为seek的修改是在读取线程里,和这里的解码线程不是一个,所以seek的修改可以在这里代码的任何位置出现。
- if (d->queue->serial == d->pkt_serial)这个判断里面为代码块1,while (d->queue->serial != d->pkt_serial)这个循环为代码块2,if (pkt.data == flush_pkt.data)这个判断为true为代码块3,false为代码块4.
- 如果seek修改出现在代码块2之前,那么就一定会进代码块2,因为packet_queue_get_or_buffering会一直读取到flush_pkt,所以也就会一定进代码块3,会执行avcodec_flush_buffers清空解码器的缓存。
- 如果seek在代码块2之后,那么就只会进代码块4,但是再循环回去时,会进代码块2、代码块3,然后avcodec_flush_buffers把这个就得packet清掉了。
- 综合上面两种情况,只有seek之后的packet才会得到解码,牛逼!
这一段厉害在:
- seek的修改在任何时候,它都不会出错
- seek的处理是在解码线程里做的,省去了条件锁等线程间通信的处理,更简单稳定。如果整个数据流是一条河流,那flush_pkt就像一个这个河流的一个浮标,遇到这个浮标,后面水流的颜色都变了。有一种自己升级自己的这种意思,而不是由一个第三方来做辅助的升级。对于流水线式的程序逻辑,这样做更好。
4.播放处
视频video_refresh里:    if (vp->serial != is->videoq.serial) {        frame\_queue\_next(&is->pictq);        goto retry;    }
音频audio_decode_frame里:     do {        if (!(af = frame\_queue\_peek_readable(&is->sampq)))            return -1;        frame\_queue\_next(&is->sampq);     } while (af->serial != is->audioq.serial);
都根据serial把旧数据略过了。
所以整体看下来,seek体系里最厉害的东西的东西就是使用了serial来标记数据,从而可以很明确的知道哪些是就数据,哪些是新数据。然后处理都是在原线程里做的处理,而不是在另外的线程里来修改相关的数据,省去了线程控制、线程通讯的麻烦的操作,稳定性也提高了。
播放时间获取
看ijkmp_get_current_position,seek时,返回seek的时间,播放时看ffp_get_current_position_l,核心就是内容时间get_master_clock减去开始时间is->ic->start_time。
seek的时候,内容位置发生了一个巨大的跳跃,所以要怎么维持同步钟的正确?
音频和视频数据里的pts都是frame->pts * av_q2d(tb),也就是内容时间,但是转成了现实时间单位。
然后is->audio_clock = af->pts + (double) af->frame->nb_samples / af->frame->sample_rate;,所以is->audio_clock是最新的一帧音频的数据播完时内容时间
在音频的填充方法里,设置音频钟的代码是: set\_clock\_at(&is->audclk,  is->audio\_clock - (double)(is->audio\_write\_buf\_size) / is->audio\_tgt.bytes\_per\_sec - SDL\_AoutGetLatencySeconds(ffp->aout),  is->audio\_clock\_serial,  ffp->audio\_callback\_time / 1000000.0);
因为is->audio_write_buf_size = is->audio_buf_size - is->audio_buf_index;,所以audio_write_buf_size就是当前帧还没读完剩余的大小,所以(double)(is->audio_write_buf_size) / is->audio_tgt.bytes_per_sec就标识剩余的数据播放完的时间。
SDL_AoutGetLatencySeconds(ffp->aout)是上层的缓冲区的数据的时间,对iOS的AudioQueue而言,有多个AudioBuffer等待播放,这个时间就是它们播放完要花的时间。
时间轴上是这样的:
[帧结束点][剩余buf时间][上层的buf时间][刚结束播放的点]
所以第二个参数的时间是:当前帧结束时的内容时间-剩余buf的时间-上层播放器buf的时间,也就是刚结束播放的内容时间。
ffp->audio_callback_time是填充方法调用时的时间,这里存在一个假设,就是上层播放器播完了一个buffer,立马调用了填充函数,所以ffp->audio_callback_time就是刚结束播放的现实时间。
这样第2个参数和第4个参数的意义就匹配上了。
回到seek,在seek完成后,会有第一个新的frame进入播放,它会把同步钟的pts,也就是媒体的内容时间调整到seek后的位置,那么还有一个问题:mp->seek_req这个标识重置回0的时间点必须比第一个新frame的set_clock_at要晚,否则同步钟的时间还没调到新的,seek的标识就结束了,然后根据同步钟去计算当前的播放时间,就出错了(界面上应该是进度条闪回seek之前)。
而事实上并没有这样,因为在同步钟的get_clock,还有一个
if (*c->queue_serial != c->serial)
       return NAN;
这个serial真是神操作,太好用了!
音频钟和视频钟的serial都是在播放时更新的,也就是第一帧新数据播放时更新到seek以后的serial,而c->queue_serial是一个指针:init_clock(&is->vidclk, &is->videoq.serial);,和packetQueue的serial共享内存的。所以也就是到第一帧新数据播放后,c->queue_serial != c->serial这个才不成立。也就是即使mp->seek_req重置回0,取得值还是seek的目标值,还不是根据pts计算的,所以也不会闪回了。
**stop时的资源释放**
从方法shutdown到核心释放方法stream_close。操作的流程如下:
1、停掉读取线程:
packet_queue_abort把音视频的packetQueue停止读取
abort_request标识为1,然后SDL_WaitThread等待线程结束
2、停掉解码器部分stream_component_close:
- decoder_abort停掉packetQueue,放开framequeue的阻塞,等待解码线程结束,然后清空packetQueue。
- decoder_destroy 销毁解码器
- 重置流数据为空
3、停掉显示线程:在显示线程里有判断数据流,视频is->video_st,音频is->audio_st,在上一步里把流重置为空,显示线程会结束。这里同样使用SDL_WaitThread等待线程结束。
4、清空缓冲区数据:packet_queue_destroy销毁packetQueue,frame_queue_destory销毁frameQueue。
对比我写的,需要修改的地方:
- 结束线程使用pthread_join的方式,而不是用锁
- 解码器、缓冲区等全部摧毁,下次播放再重建,不要重用
- 音频的停止通过停掉上层播放器,底层是被动的,而且没有循环线程;视频的停止也只需要等待线程结束。
核心就是第一点,使用pthread_join等待线程结束。
网络不好处理
会自动暂停,等待。内部可以控制播放或暂停。
使用VTB时架构的统一
- frame缓冲区使用自定义的数据结构Frame,通过他可以把各种样式进行统一。
- 下层拥有了Frame数据,上层的对接对象时Vout,边界就在这里。然后上层要的是overlay,所以问题就是怎么由frame转化成overlay,以及如何显示overlay。这两个操作由Vout提供的create_overlay和display_overlay来完成。
- 使用VTB之后,数据存在解码后获得的pixelBuffer里,而ffmpeg解码后的数据在AVFrame里,这个转化的区别就在不同的overlay创建函数里。
总结:
- 对于两个模块的连接处,为了统一,两边都需要封装统一的模型;
- 在统一的模型内,又具有不同的操作细分;
- 输入数据从A到B,那么细分操作由B来提供,应为B是接受者,它知道需要一个什么样的结果。
- 这样在执行流程上一样的,能保持流程的稳定性;而实际执行时,在某些地方又有不同,从而又可以适应各种独特的需求。
原创作者:FindCrt,原文链接: https://www.jianshu.com/p/814f3a0ee997
欢迎关注我的微信公众号「码农突围」,分享Python、Java、大数据、机器学习、人工智能等技术,关注码农技术提升•职场突围•思维跃迁,20万+码农成长充电第一站,陪有梦想的你一起成长。
多媒体
2020-03-09 19:54:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
PS快捷键(2020版)
No.1
工具箱
1.矩形、椭圆选框工具 【M】
2.裁剪工具 【C】
3.移动工具 【V】
4.画笔工具 【B】
5.橡皮图章、图案图章 【S】
6.历史记录画笔工具 【Y】
7.橡皮擦工具 【E】
8.模糊、锐化、涂抹工具 【R】
9.减淡、加深、海棉工具 【O】
10.钢笔、自由钢笔、磁性钢笔 【P】
11.油漆桶工具 【G】
12.吸管、颜色取样器 【I】
13.抓手工具 【H】
14.临时使用移动工具 【Ctrl】
15.临时使用吸色工具 【Alt】
16.临时使用抓手工具 【空格】
17.添加锚点工具 【+】
18.删除锚点工具 【-】
19.直接选取工具 【A】
20.文字、文字蒙板、直排文字、直排文字蒙板 【T】
21.直线渐变、径向渐变、对称渐变、角度渐变、菱形渐变 【G】
22.缩放工具 【Z】
23.默认前景色和背景色 【D】
24.切换前景色和背景色 【X】
25.切换标准模式和快速蒙板模式 【Q】
26.标准屏幕模式、带有菜单栏的全屏模式、全屏模式 【F】
27.快速输入工具选项(当前工具选项面板中至少有一个可调节数字) 【0】 至 【9】
28.循环选择画笔 【[】 或 【]】
29.选择第一个画笔 【Shift】 + 【[】
30.选择最后一个画笔 【Shift】 + 【]】
No.2
文件操作
1.新建图形文件 【Ctrl】+【N】
2.新建图层 【Ctrl】+【Shift】+【N】
3.用默认设置创建新文件 【Ctrl】+【Alt】+【N】
4.打开已有的图像 【Ctrl】+【O】
5.打开为... 【Ctrl】+【Alt】+【O】
6.关闭当前图像 【Ctrl】+【W】
7.保存当前图像 【Ctrl】+【S】
8.另存为... 【Ctrl】+【Shift】+【S】
9.存储副本 【Ctrl】+【Alt】+【S】
10.页面设置 【Ctrl】+【Shift】+【P】
11.打印 【Ctrl】+【P】
12.打开“预置”对话框 【Ctrl】+【K】
13.显示最后一次显示的“预置”对话框 【Alt】+【Ctrl】+【K】
No.3
图层混合
1.循环选择混合模式 【Alt】+【-】或【+】
2.正常 【Shift】+【Alt】+【N】
3.阈值(位图模式) 【Shift】+【Alt】+【L】
4.溶解 【Shift】+【Alt】+【I】
5.背后 【Shift】+【Alt】+【Q】
6.清除 【Shift】+【Alt】+【R】
7.正片叠底 【Shift】+【Alt】+【M】
8.屏幕 【Shift】+【Alt】+【S】
9.叠加 【Shift】+【Alt】+【O】
10.柔光 【Shift】+【Alt】+【F】
11.强光 【Shift】+【Alt】+【H】
12.滤色 【Shift】+【Alt】+【S】
13.颜色减淡 【Shift】+【Alt】+【D】
14.颜色加深 【Shift】+【Alt】+【B】
15.变暗 【Shift】+【Alt】+【K】
16.变亮 【Shift】+【Alt】+【G】
17.差值 【Shift】+【Alt】+【E】
18.排除 【Shift】+【Alt】+【X】
19.色相 【Shift】+【Alt】+【U】
20.饱和度 【Shift】+【Alt】+【T】
21.颜色 【Shift】+【Alt】+【C】
22.光度 【Shift】+【Alt】+【Y】
23.去色海棉工具 【Shift】+【Alt】+【J】
24.加色海棉工具 【Shift】+【Alt】+【A】
25.暗调减淡/加深工具 【Shift】+【Alt】+【W】
26.中间调减淡/加深工具 【Shift】+【Alt】+【V】
27.高光减淡/加深工具 【Shift】+【Alt】+【Z】
No.4
选择功能
1.全部选取 【Ctrl】+【A】
2.取消选择 【Ctrl】+【D】
3.重新选择 【Ctrl】+【Shift】+【D】
4.羽化选择 【Ctrl】+【Alt】+【D】
5.反向选择 【Ctrl】+【Shift】+【I】
6.路径变选区数字键盘的 【Enter】
7.载入选区 【Ctrl】 +点按图层、路径、通道面板中的缩约图
No.5
滤镜
1.按上次的参数再做一次上次的滤镜 【Ctrl】+【F】
2.退去上次所做滤镜的效果 【Ctrl】+【Shift】+【F】
3.重复上次所做的滤镜(可调参数) 【Ctrl】+【Alt】+【F】
4.选择工具(在“3D变化”滤镜中) 【V】
5.立方体工具(在“3D变化”滤镜中) 【M】
6.球体工具(在“3D变化”滤镜中) 【N】
7.柱体工具(在“3D变化”滤镜中) 【C】
8.轨迹球(在“3D变化”滤镜中) 【R】
9.全景相机工具(在“3D变化”滤镜中) 【E】
10.向上卷动一屏 【PageUp】
11.向下卷动一屏 【PageDown】
12.向左卷动一屏 【Ctrl】+【PageUp】
No.6
编辑操作
1.还原/重做前一步操作 【Ctrl】+【Z】
2.还原两步以上操作 【Ctrl】+【Alt】+【Z】
3.重做两步以上操作 【Ctrl】+【Shift】+【Z】
4.剪切选取的图像或路径 【Ctrl】+【X】或【F2】
5.拷贝选取的图像或路径 【Ctrl】+【C】
6.合并拷贝 【Ctrl】+【Shift】+【C】
7.将剪贴板的内容粘到当前图形中 【Ctrl】+【V】或【F4】
8.将剪贴板的内容粘到选框中 【Ctrl】+【Shift】+【V】
9.自由变换 【Ctrl】+【T】
10.应用自由变换(在自由变换模式下) 【Enter】
11.从中心或对称点开始变换 (在自由变换模式下) 【Alt】
12.限制(在自由变换模式下) 【Shift】
13.扭曲(在自由变换模式下) 【Ctrl】
14.取消变形(在自由变换模式下) 【Esc】
15.自由变换复制的象素数据 【Ctrl】+【Shift】+【T】
16.再次变换复制的象素数据并建立一个副本 【Ctrl】+【Shift】+【Alt】+【T】
17.删除选框中的图案或选取的路径 【DEL】
18.用背景色填充所选区域或整个图层 【Ctrl】+【BackSpace】或【Ctrl】+【Del】
19.用前景色填充所选区域或整个图层 【Alt】+【BackSpace】或【Alt】+【Del】
20.弹出“填充”对话框 【Shift】+【BackSpace】或【shift】+【F5】
21.从历史记录中填充 【Alt】+【Ctrl】+【Backspace】
No.7
视图操作
1.显示彩色通道 【Ctrl】+【~】
2.显示单色通道 【Ctrl】+【数字】
3.显示复合通道 【~】
4.以CMYK方式预览(开) 【Ctrl】+【Y】
5.打开/关闭色域警告 【Ctrl】+【Shift】+【Y】
6.放大视图 【Ctrl】+【+】
7.缩小视图 【Ctrl】+【-】
8.满画布显示 【Ctrl】+【0】
9.实际象素显示 【Ctrl】+【Alt】+【0】
10.向上卷动一屏 【PageUp】
11.向下卷动一屏 【PageDown】
12.向左卷动一屏 【Ctrl】+【PageUp】
13.向右卷动一屏 【Ctrl】+【PageDown】
14.向上卷动10个单位 【Shift】+【PageUp】
15.向下卷动10个单位 【Shift】+【PageDown】
16.向左卷动10个单位 【Shift】+【Ctrl】+【PageUp】
17.向右卷动10个单位 【Shift】+【Ctrl】+【PageDown】
18.将视图移到左上角 【Home】
19.将视图移到右下角 【End】
20.显示/隐藏选择区域 【Ctrl】+【H】
21.显示/隐藏路径 【Ctrl】+【Shift】+【H】
22.显示/隐藏标尺 【Ctrl】+【R】
23.显示/隐藏参考线 【Ctrl】+【;】
24.显示/隐藏网格 【Ctrl】+【”】
25.贴紧参考线 【Ctrl】+【Shift】+【;】
26.锁定参考线 【Ctrl】+【Alt】+【;】
27.贴紧网格 【Ctrl】+【Shift】+【”】
28.显示/隐藏“画笔”面板 【F5】
29.显示/隐藏“颜色”面板 【F6】
30.显示/隐藏“图层”面板 【F7】
31.显示/隐藏“信息”面板 【F8】
32.显示/隐藏“动作”面板 【F9】
33.显示/隐藏所有命令面板 【TAB】
34.显示或隐藏工具箱以外的所有调板 【Shift】+【TAB】
35.文字处理(在”文字工具”对话框中)
36.左对齐或顶对齐 【Ctrl】+【Shift】+【L】
37.中对齐 【Ctrl】+【Shift】+【C】
38.右对齐或底对齐 【Ctrl】+【Shift】+【R】
39.左/右选择1个字符 【Shift】+【←】/【→】
40.下/上选择1行 【Shift】+【↑】/【↓】
41.选择所有字符 【Ctrl】+【A】
42.选择从插入点到鼠标点按点的字符 **【Shift】**加点按
43.左/右移动1个字符 【←】/【→】
44.下/上移动1行 【↑】/【↓】
45.左/右移动1个字 【Ctrl】+【←】/【→】
46.将所选文本的文字大小减小2点象素 【Ctrl】+【Shift】+【<】
47.将所选文本的文字大小增大2点象素 【Ctrl】+【Shift】+【>】
48.将所选文本的文字大小减小10点象素 【Ctrl】+【Alt】+【Shift】+【<】
49.将所选文本的文字大小增大10点象素 【Ctrl】+【Alt】+【Shift】+【>】
50.将行距减小2点象素 【Alt】+【↓】
51.将行距增大2点象素 【Alt】+【↑】
52.将基线位移减小2点象素 【Shift】+【Alt】+【↓】
53.将基线位移增加2点象素 【Shift】+【Alt】+【↑】
54.将字距微调或字距调整减小 20/1000ems 【Alt】+【←】
55.将字距微调或字距调整增加 20/1000ems 【Alt】+【→】
56.将字距微调或字距调整减小 100/1000ems 【Ctrl】+【Alt】+【←】
57.将字距微调或字距调整增加 100/1000ems 【Ctrl】+【Alt】+【→】
No.8
图层操作
1.从对话框新建一个图层 【Ctrl】+【Shift】+【N】
2.以默认选项建立一个新的图层 【Ctrl】+【Alt】+【Shift】+【N】
3.通过拷贝建立一个图层 【Ctrl】+【J】
4.通过剪切建立一个图层 【Ctrl】+【Shift】+【J】
5.与前一图层编组 【Ctrl】+【G】
6.取消编组 【Ctrl】+【Shift】+【G】
7.向下合并或合并联接图层 【Ctrl】+【E】
8.合并可见图层 【Ctrl】+【Shift】+【E】
9.盖印或盖印联接图层 【Ctrl】+【Alt】+【E】
10.盖印可见图层 【Ctrl】+【Alt】+【Shift】+【E】
11.将当前层下移一层 【Ctrl】+【[】
12.将当前层上移一层 【Ctrl】+【]】
13.将当前层移到最下面 【Ctrl】+【Shift】+【[】
14.将当前层移到最上面 【Ctrl】+【Shift】+【]】
15.激活下一个图层 【Alt】+【[】
16.激活上一个图层 【Alt】+【]】
17.激活底部图层 【Shift】+【Alt】+【[】
18.激活顶部图层 【Shift】+【Alt】+【]】
19.调整当前图层的透明度(当前工具为无数字参数的,如移动工具) 【0】 至 【9】
20.保留当前图层的透明区域(开关) 【/】
No.9
图像调整
1.调整色阶 【Ctrl】+【L】
2.自动调整色阶 【Ctrl】+【Shift】+【L】
3.打开曲线调整对话框 【Ctrl】+【M】
4.在所选通道的曲线上添加新的点(‘曲线’对话框中) 在图象中 **【Ctrl】**加点按
5.在复合曲线以外的所有曲线上添加新的点(‘曲线’对话框中) 【Ctrl】+【Shift】
No.10
曲线操作
1.移动所选点(‘曲线’对话框中) 【↑】/【↓】/【←】/【→】
2.以10点为增幅移动所选点以10点为增幅(‘曲线’对话框中) 【Shift】+【箭头】
3.选择多个控制点(‘曲线’对话框中) **【Shift】**加点按键
4.前移控制点(‘曲线’对话框中) 【Ctrl】+【Tab】
5.后移控制点(‘曲线’对话框中) 【Ctrl】+【Shift】+【Tab】
6.添加新的点(‘曲线’对话框中) 点按网格
7.删除点(‘曲线’对话框中) **【Ctrl】**加点按键
8.取消选择所选通道上的所有点(‘曲线’对话框中) 【Ctrl】+【D】
9.使曲线网格更精细或更粗糙(‘曲线’对话框中) **【Alt】**加点按网格
10.选择彩色通道(‘曲线’对话框中) 【Ctrl】+【~】
11.选择单色通道(‘曲线’对话框中) 【Ctrl】+【数字】
12.打开“色彩平衡”对话框 【Ctrl】+【B】
13.打开“色相/饱和度”对话框 【Ctrl】+【U】
14.全图调整(在色相/饱和度”对话框中) 【Ctrl】+【~】
15.只调整红色(在色相/饱和度”对话框中) 【Ctrl】+【1】
16.只调整黄色(在色相/饱和度”对话框中) 【Ctrl】+【2】
17.只调整绿色(在色相/饱和度”对话框中) 【Ctrl】+【3】
18.只调整青色(在色相/饱和度”对话框中) 【Ctrl】+【4】
19.只调整蓝色(在色相/饱和度”对话框中) 【Ctrl】+【5】
20.只调整洋红(在色相/饱和度”对话框中) 【Ctrl】+【6】
21.去色 【Ctrl】+【Shift】+【U】
22.反相 【Ctrl】+【I】
多媒体
2020-03-09 15:36:02
「深度学习福利」大神带你进阶工程师,立即查看>>>
PS快捷键(Mac OS)
常用工具
侧边栏工具 V 移动工具 M 矩形选框工具 L 套索、多边形套索、磁性套索 C 裁剪工具 I 吸管、颜色取样器(在选中其他工具时,按住I可以临时吸取色值) J 污点修复工具 B 画笔工具 在 B 下,调整笔头大小:“中括号”左 [ 右 ] ; 在 B 下,调整笔柔角:Shift+“中括号”; S 仿制图章、图案图章 M 魔棒工具 Y 历史记录画笔工具 E 像皮擦工具 G 油漆桶 R 模糊、锐化、涂抹工具 O 减淡、加深、海棉工具(处理图片时神器) P 钢笔、自由钢笔、磁性钢笔(按住com键临时切换成路径选择,选点调整) T 文字 A 路径、直接选取工具 M 矩形、椭圆选择工具 H 抓手 R 旋转视图 Z 缩放工具(默认放大按alt临时切换成缩小工具) D 默认前景色和背景色 X 切换前景色和背景色 Q 切换标准模式和快速蒙板模式 ⌃ 临时使用移动工具 ⌥ 临时使用吸色工具 空格 临时使用抓手工具
选择 ⌘A 文字选中时,全选文字 ⌘单击图层 将图层转换为选择区 ⇧⌘I 选择区反选 ⌘T 变形 ⌘D 取消选择区 方向键 选择区域移动 ⇧方向键 选择区域以10个像素为单位移动 ⌥方向键 复制选中的图层 ⌘⌥⇧T 快速复制上一次动作 ⌥⌫ 填充为前景色 ⌘⌫ 填充为背景色 ⇧⌘E 合并可见图层 ⌘E 合并选中图层
色彩处理 ⌘L 调整图片色阶工具 ⌘B 调整图片色彩平衡 ⌘U 调节图片色调/饱和度 ⌘F 重复使用滤镜
查看及文件处理 ⌘+ 放大视窗 ⌘- 缩小视窗 ⌘] 图层向上一层 ⌘[ 图层向下一层 ⇧⌘] 图层至于最顶层 ⇧⌘[ 图层至于最底层 ⌘空格键和鼠标单击 放大局部 ⌥空格键和鼠标单击 缩小局部 ⌘R 显示或隐藏标尺 ⌘H 显示或隐藏虚线 ⌘H 显示或隐藏网格 ⌘O 打开文件 ⌘W 关闭文件 ⌘S 文件存盘 ⌘P 打印文件 ⌘Z 恢复到上一步(回到上一步,在com+z,返回当前步骤) ⇧⌘Z 继续撤销 F 标准屏幕模式、带有菜单栏的全屏模式、全屏模式(小屏幕实用技能)
多媒体
2020-03-09 15:26:09
「深度学习福利」大神带你进阶工程师,立即查看>>> 闲暇之余 整理往日照片 ,历经了 exiv2 / exiftool 重命名、 SQLite 归档 和 人工识别 。 群晖存储上有价值的文件名都已经格式化完毕,赶紧把它们 异地 备份一轮,脚本如下: #!/bin/bash # Copy.sh : Backup the files to remote storage. readonly pDir="${1:-~/XiaoMi/Image/}" # 照片文件备份路径 readonly vDir="${2:-~/XiaoMi/Video/}" # 视频文件备份路径 readonly dDir="${3:-~/XiaoMi/Document/}" # 日志存储目录 readonly iLog="${dDir}Copy.log" # 备份成功 readonly wLog="${dDir}Copy.wan" # 已经存在 readonly eLog="${dDir}Copy.err" # 无需备份和出错信息 readonly debug=false # 调试开关,按需开启 readonly stdName="(^|[[:space:]])[2,1][0,9][0,1,2,7,9][0-9][0-1][0-9][0-3][0-9]-[0-2][0-9][0-5][0-9][0-5][0-9]($|[[:space:]])" # 标准格式 find ~{higkoo,anglix}/Drive/{Moments,Backup} ! -path "*@eaDir*" -type f | while read -r sPath; do unset fName sExt rCopy rCode nDir dPath sExt="${sPath##*.}" && sExt="${sExt,,}" [[ ${sExt} == "jpeg" ]] && sExt='jpg' # 将jpeg后缀改为jpg fName="${sPath##*/}" && fName="${fName%%.*}" if [[ $fName =~ $stdName ]]; then # 识别文件名 case $sExt in jpg | livp | heic | cr2 ) nDir="${pDir}${fName:0:4}/" rCopy=true && rCode='+' ;; mp4 | mov | m4v | wmv ) nDir="${vDir}${fName:0:4}/" rCopy=true && rCode='-' ;; png | * ) # 鄙视 png ,全是截图 rCopy=false && rCode='*' ;; esac else rCopy=false && rCode='/' fi if [[ $rCopy && ! -z ${nDir} ]]; then dPath="${nDir}${fName}.${sExt}" if [[ ! -f "${dPath}" || `stat --printf=%s "${dPath}" 2>/dev/null` -lt `stat --printf=%s "${sPath}"` ]]; then mkdir -pv "${nDir}" >> ${wLog} cp -v "${sPath}" "${dPath}" >>${iLog} 2>>${eLog} else echo "$sPath" >> ${wLog} && rCode='^' fi else echo "$sPath" >> ${eLog} fi echo -ne "$rCode" $debug && declare -p sPath dPath && exit 0 done 照片体积小,相对容易移动、分享,所以和视频独立开、均按年分目录。 脚本经过千锤百炼、始出来,让它跑一会:
之前用 exiv2 重命名后备份过一次,所以不少 ^ 以示逃过。 + 代表收获1张新照片, - 代表收获1张新视频, / 是不必备份的文件。 至此,照片/视频整理工作完毕、剩下就是给它们安全的备份起来(略)。 新建了个简单的数据库,用于记录文件的走留: CREATE TABLE if not exists TMission ( FullPath VARCHAR(100) PRIMARY KEY NOT NULL, FileMd5 NVARCHAR(33) NOT NULL, FileSize INT NOT NULL, FileTime INTEGER, FileCode INT2, UpdateTime INTEGER ); CREATE INDEX if not exists FullPath ON TMission (FullPath); CREATE INDEX if not exists FileMd5 ON TMission (FileMd5); CREATE INDEX if not exists FileTime ON TMission (FileTime); CREATE INDEX if not exists FileStauts ON TMission (FileCode); CREATE INDEX if not exists UpdateTime ON TMission (UpdateTime); /* FileCode 说明 * 0 合规文件 * 1 归档文件 * 11 无需归档(文件名不合规) * 12 无需归档(从归档文件中手动删除) * 13 无需归档(手动排除不作归档) */ 把之前扫描过的文件信息合并进来, nohup ~admin/www/TMission/Merge.sh "/volume1/homes/higkoo/Drive/Backup/" > debug.log 2>&1 & : #!/bin/bash # Merge.sh: 将之前扫描过的文件信息合并到新数据库 readonly debug=false # 调试开关 sDir="${1:-/volume1/homes/higkoo/Drive/Backup/}" # 扫描目录 readonly oDB='/volume1/homes/higkoo/Drive/Backup/FileList/FileInfo.db' readonly iDB="${2:-/volume1/homes/admin/www/TMission/TMission.db}" # SQLite3数据库 readonly eLog="${iDB%/*}/Error.log" # 数据库目录同级存错误日志 readonly stdName="(^|[[:space:]])[2,1][0,9][0,1,2,7,9][0-9][0-1][0-9][0-3][0-9]-[0-2][0-9][0-5][0-9][0-5][0-9]($|[[:space:]])" # 标准格式 find "${sDir}" ! -path "*@eaDir*" -type f | while read sPath; do unset fMd5 fTime fSize fCode uTime fName IFS eMd5 IFS='|' read fMd5 fSize < <(sqlite3 "$oDB" "select FileMd5,FileSize from FileInfo where FullPath='${sPath:-NULL}' limit 1" 2>>"$eLog") [[ -z $fMd5 ]] && fMd5=`md5sum "${sPath}"` && fMd5="${fMd5%% *}" [[ -z $fSize ]] && fSize=`stat --printf=%s "${sPath}"` fName="${sPath##*/}" && fName="${fName%.*}" if [[ $fName =~ $stdName ]]; then fTime=${fName/-/} && fCode=0 else fCode=11 fi $debug && declare -p sPath fMd5 fTime fSize fName if [[ ! -z $fMd5 && ! -z "$sPath" && ! -z $fSize ]]; then uTime=`date +%Y%m%d%H%M%S` $debug && echo "sqlite3 $iDB \"select FileMd5 from FileInfo where FullPath='${sPath:-NULL}'\"" eMd5=`sqlite3 "$iDB" "select FileMd5 from TMission where FullPath='${sPath:-NULL}'" 2>>${eLog}` if [[ -z "$eMd5" ]]; then $debug && echo "sqlite3 ${iDB} \"INSERT INTO TMission (FullPath,FileMd5,FileSize,FileTime,FileCode,UpdateTime) VALUES ('$sPath','$fMd5','$fSize','$fTime','${fCode:-0}','$uTime');\"" sqlite3 "${iDB}" "INSERT INTO TMission (FullPath,FileMd5,FileSize,FileTime,FileCode,UpdateTime) VALUES ('$sPath','$fMd5','$fSize','$fTime','${fCode:-0}','$uTime');" 2>>"${eLog}" [[ $? -eq 0 ]] && echo -ne '+' || echo -ne '#' else echo -ne '-' fi else echo "$(date +%F\ %T),${sPath}" >>"${eLog}" echo -ne '*' fi $debug && exit 0 done
多媒体
2020-03-07 20:52:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
写在前面 疫情期间整理 Synology 里存储了 10多年的相片 ,这些相片穿越了功能手机到智能手机的年代。 不少照片非常有年代感,文件名同样也有年代感。五花八门…… 最初用 exiv2 来读取照片的源信息,发现有些老照片没有源信息、新格式的照片读不到源信息。 然后用 exiftool 来识别了新照片的源信息和部分视频的源信息。 最后甚至还启用了 SQLite 数据库存储文件的源信息。
化繁为简 今日扫描了一遍文件系统,结合 SQLite 里的数据分析,发现仍有大量 照片/视频 的拍摄时间无法确定: 部分手机拍摄的全景照片 部分第三方拍照软件 从第三方软件另存的照片 部分视频文件 很久很久以前的照片 这些都是最美好的记忆,我要保存好它。所以我决定写个脚本去识别那个“乌七八糟”不规则的文件名,哪怕会有些不需要的照片也被整理进来。 总比把珍贵的照片现在就丢失了的好,未来还可以用图片识别去分类、去重,最终无法用程序处理的再人肉处理。想想都完美~   好,开始动手敲: #!/bin/bash # Rename.sh : Foramt the file name by rules readonly iLog="./Rename.log" # 扫描日志 readonly eLog="./Rename.err" readonly debug=false readonly stdName="(^|[[:space:]])[2,1][0,9][0,1,2,7,9][0-9][0-1][0-9][0-3][0-9]-[0-2][0-9][0-5][0-9][0-5][0-9]($|[[:space:]])" # 标准格式 readonly er1Name="[0,3-9][0-9]{16,}" # 超长数字,提前过滤避免被错误解析 readonly er2Name="[0-9]{18,}" # 异长数字,提前过滤避免被错误解析 readonly er3Name="(^|[[:space:]])-[0-9]{6,}" # 长负数,提前过滤避免被错误解析 readonly minTime=$(date -d '2000-01-11 22:33:44' +%s) # 不存在比这个时间更早的文件 readonly nt1Name="[2,1][0,9][0,1,7,9][0-9][0-1][0-9][0-3][0-9]_[0-2][0-9][0-5][0-9][0-5][0-9]" # 包含 20090516_105959 readonly nt2Name="[2,1][0,9][0,1,7,9][0-9][0-1][0-9][0-3][0-9][0-2][0-9][0-5][0-9][0-5][0-9]" # 包含 20180916112233 readonly nt3Name="[2,1][0,9][0,1,7,9][0-9]_[0-1][0-9]_[0-3][0-9]_[0-2][0-9]_[0-5][0-9]_[0-5][0-9]" # 包含 2018_09_16_11_22_33 readonly nt4Name="[2,1][0,9][0,1,7,9][0-9]-[0-1][0-9]-[0-3][0-9] [0-2][0-9][0-5][0-9][0-5][0-9]" # 包含 2009-05-16 223344 readonly nt5Name="[2,1][0,9][0,1,7,9][0-9]-[0-1][0-9]-[0-3][0-9]-[0-2][0-9]-[0-5][0-9]-[0-5][0-9]" # 包含 2018-09-16-11-22-33 readonly secName="[1,9][0-9]{8,9}" # 包含秒级时间戳,支持2000年之后 find /volume1/homes/{higkoo,anglix}/Drive/{Backup,Moments} ! -path "*@eaDir*" -type f | while read -r sPath; do unset fName fExt nName rCode rMove tStr fExt="${sPath##*.}" [[ ${fExt,,} =~ "png" ]] && continue # 丢弃截图 [[ ${sExt,,} == "jpeg" ]] && sExt='jpg' # 将jpeg后缀改为jpg fName="${sPath##*/}" && fName="${fName%%.*}" if [[ $fName =~ $stdName ]]; then # case 的正则弱于 [[ ]] ,所以这里使用 if 嵌套 rCode=0 && rMove=false && echo -ne '.' elif [[ $fName =~ $er1Name || $fName =~ $er2Name || $fName =~ $er3Name ]]; then rCode='A' && rMove=false && echo -ne '#' echo "$sPath" >> ${eLog} elif [[ $fName =~ $nt1Name ]]; then rCode=1 && rMove=true && echo -ne "$rCode" nName="${BASH_REMATCH[0]/_/-}" elif [[ $fName =~ $nt2Name ]]; then rCode=2 && rMove=true && echo -ne "$rCode" nName="${BASH_REMATCH[0]:0:8}-${BASH_REMATCH[0]:8:6}" elif [[ $fName =~ $nt3Name ]]; then rCode=3 && rMove=true && echo -ne "$rCode" nName="${BASH_REMATCH[0]//_/}" && nName="${nName:0:8}-${nName:8:6}" elif [[ $fName =~ $nt4Name ]]; then rCode=4 && rMove=true && echo -ne "$rCode" nName="${BASH_REMATCH[0]//-/}" && nName="${nName/ /-}" elif [[ $fName =~ $nt5Name ]]; then rCode=5 && rMove=true && echo -ne "$rCode" nName="${BASH_REMATCH[0]//-/}" && nName="${nName:0:8}-${nName:8:6}" elif [[ $fName =~ $secName ]]; then tStr="${BASH_REMATCH[0]}" if [[ $tStr -le $minTime || $tStr -ge $(date +%s) ]]; then rCode='B' && rMove=false && echo -ne '-' echo "$sPath" >> ${eLog} else rCode=8 && echo -ne "$rCode" nName="$(date -d @$tStr +%Y%m%d-%H%M%S)" fi else rCode='E' && echo -ne '*' echo "$sPath" >> ${eLog} continue fi $rMove && mv -v "${sPath}" "${sPath%/*}/${nName}.${fExt,,}" >>${iLog} 2>>${eLog} $debug && declare -p rCode BASH_REMATCH && exit 0 done 脚本很快就跑完了,成功找回近 7657 张相片/视频。 还剩下两千多张,看了下文件名确实无法判断。后期人肉处理,先开心一会~
多媒体
2020-03-07 00:26:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
功夫不负有心人,自己魔改翻新后的 Ambulant Player 终于构建成功了。
理论上把代码克隆下来然后运行 build.sh 就行。可能会缺 ed 等依赖,按照控制台输出提示补全就好,至少坑没有我魔改之前那么多了。构建成功生成的二进制可执行文件位于 src/player_gtk/AmbulantPlayer_gtk 。
这大概就是一个春风得意的构建骨灰级玩家最形象的写照吧。
2015年的老古董终于复活啦!继 Animata 以后再次构建成功!
有点失望的是 Ambulant Player 的界面比 Garlic Player 复杂不了多少,但是整个过程学习到了 FFmpeg API 的演变史 及自 FFmpeg 2.3 以来 AVCodecContext 类的 codec_name 字段就被弃用,后者在网上的信息是寥寥无几,根本无人解释原因,只有亲自翻翻各版本的 FFmpeg Doxygen 文档对比以后才知道。
目前已经上传到 AUR 。构建以后运行会提示缺少一个链接库,其实只要创建个软链接就好,自己在系统库里找到真正的库文件,然后软链接成它需要的文件名就好。本人先偷个懒,关注的人多的话就弄。
后来尝试在 Windows 下用 VS 2008 构建,然而提示缺 d2_player.h 等头文件,不懂现在这些头文件上哪弄去,如果有知道的同学,恳请不吝告知!
多媒体
2020-03-05 19:27:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
写在前面 前面借助 exiv2 和 exiftool 做照片视频做了归档,是时候把需要的信息存起来了。 考虑到数据的安全性、读写的便利性,决定用 SQLite ( Synology 内置)。 注: Synology 还内置了 Redis 、 PostGreSQL 。
建库 文件系统,只有文件全路径是唯一,其它信息均读出来方便日后查: CREATE TABLE if not exists FileInfo ( FullPath NVARCHAR(100) PRIMARY KEY NOT NULL, FileMd5 NCHAR(33) NOT NULL, FileType CHARACTER(9), FileSize INT NOT NULL, CreateTime DATETIME, UpdateTime DATETIME ); CREATE INDEX if not exists FullPath ON FileInfo (FullPath); CREATE INDEX if not exists CreateTime ON FileInfo (CreateTime); CREATE INDEX if not exists FileType ON FileInfo (FileType); CREATE INDEX if not exists FileMd5 ON FileInfo (FileMd5); 增删改查总得调试几把: sqlite3 ~admin/www/Tmission/FileInfo.db 'select count(FullPath) from FileInfo where FullPath like "%王阳明故居%";' 输出 Excel 也挺方便: sqlite3 -header -csv ~admin/www/Tmission/FileInfo.db "select FullPath,datetime(CreateTime,'unixepoch','localtime') from FileInfo where FullPath like '%国剧少年行%' limit 3" # FullPath,"datetime(CreateTime,'unixepoch','localtime')" # "/volume1/homes/higkoo/Drive/Backup/国剧少年行/20190109-134140.JPG","2019-01-09 13:41:40" # "/volume1/homes/higkoo/Drive/Backup/国剧少年行/20190109-125415.JPG","2019-01-09 12:54:15" # "/volume1/homes/higkoo/Drive/Backup/国剧少年行/20190109-112352.JPG","2019-01-09 11:23:52"
归档 全盘扫描,并存入必要的信息: #!/bin/bash sDir="${1:-/volume1/homes/higkoo/Drive/Backup/}" # 扫描目录 iDB="${2:-/volume1/homes/admin/www/Tmission/FileInfo.db}" # SQLite3数据库 readonly debug=true # 调试开关 readonly eLog="${iDB%%/*}Error.log" # 数据库目录同级存错误日志 find "${sDir}" ! -path "*@eaDir*" -type f | while read sPath; do unset fMd5 cTime fSize fExt uTime fExt="${sPath##*.}" fMd5=`md5sum "${sPath}"` && fMd5="${fMd5%% *}" cTime=`exiftool -d %s -DateTimeOriginal -S -s "${sPath}"` fSize=`stat --printf=%s "${sPath}"` uTime=`date +%s` $debug && declare -p sPath fExt fSize fMd5 cTime uTime if [[ ! -z $fMd5 && ! -z "$sPath" && ! -z $fSize ]]; then eMd5=`sqlite3 "${iDB}" "select FileMd5 from FileInfo where FullPath='${sPath:-NULL}'" 2>>"${eLog}"` if [[ -z "$eMd5" ]]; then sqlite3 "${iDB}" "INSERT INTO FileInfo (FileMd5,FullPath,FileSize,FileType,CreateTime,UpdateTime) VALUES ('$fMd5','$sPath','$fSize','$fExt','$cTime','$uTime');" 2>>"${eLog}" [[ $? -eq 0 ]] && echo -ne '+' || echo -ne '#' else if [[ "$eMd5" == "$fMd5" ]]; then echo -ne '_' else sqlite3 "${iDB}" "UPDATE FileInfo SET (FileMd5,FileSize,FileType,CreateTime,UpdateTime) VALUES ('$eMd5','$fSize','$fExt','$cTime','$uTime') where FullPath='${sPath:-NULL}';" 2>>"${eLog}" [[ $? -eq 0 ]] && echo -ne '-' || echo -ne '*' fi fi else echo "$(date +%F\ %T),${sPath}" >>"${eLog}" echo -ne '.' fi $debug && exit 0 done 端杯水,让程序在后台跑一会: nohup Task.sh > debug.log 2>&1 &
多媒体
2020-03-05 14:11:00
「深度学习福利」大神带你进阶工程师,立即查看>>> 上次整理照片: 强大的 exiv2 助 Synology 照片整理一臂之力 由于 exiv2 不支持 heic 图片和视频文件,联系 Synology 推荐了 exiftool 。
安装 exiftool # 先安装 Perl 套件,然后开启 ssh-server wget -o ~admin/www/Image-ExifTool-11.89.tar.gz https://exiftool.org/Image-ExifTool-11.89.tar.gz tar -C ~admin/www -zxvf ~admin/www/Image-ExifTool-11.89.tar.gz ln -s ~admin/www/Image-ExifTool-11.89/exiftool /usr/local/bin/exiftool exiftool -ver
体验 exiftool exiftool 果然 很强大 ,自带了 图片改名功能 、而且支持非常丰富的媒体类型。
# 按照片拍摄年份,存入指定目录 exiftool '-Directory用后感 exiftool exiftool 虽然强大,建议当工具使用。不少定制化的需求无法满足: 1、默认 -o 拷贝的时候,遇到没有源信息文件 会全堆在一块(异常图片处理不好) 2、不带文件重名覆盖的选项(遇到重复会改名,没有其它逻辑) 3、不支持目录排除(递归查找无法排除) 4、不输出改名后的信息,无法记录(输出信息少,嵌入脚本使用不友好) 5、改名/移动 功能不错,复制设计不足(宝贵的图片源稿不到最后不删、不动) 好在 exiftool 能补足 exiv2 功能上的不足,一行命令就能修复未识别的文件: find ~/Drive/Moments/Mobile ~/Drive/Backup/MobileDevice ! -path "*@eaDir*" -type f -iname "*.heic" -o -iname "*.cr2" -exec exiftool '-filename>"${eLog}" dfName="${spName//-/}" && dfName="${dfName// /-}.${sExt,,}" dInfo[p]="${dDir}${dInfo[Y]}/${dfName}" nPath="${sPath%/*}/${dfName}" # 新文件名 else # 从文件 Meta 信息读拍摄时间,拼接目标文件全路径 dInfo[p]=`exiftool -d "${dDir}%Y/%Y%m%d-%H%M%S.${sExt,,}" -DateTimeOriginal -S -s "${sInfo[s]"` 2>>"${eLog}" dInfo[Y]="${dInfo[p]:${#dDir}:4}" nPath="${sPath%/*}/${dInfo[p]##*/}" # 新文件名 fi mv -v "${sPath}" "${nPath}" 2>>"${eLog}" && sed -i "${lNum}s/${sPath##*/}/${dInfo[p]##*/}/" "${fList}" || rCode=4 # 更新日志 if [[ ! ${dDirList[*]} =~ (^|[[:space:]])"${dInfo[Y]}"($|[[:space:]]) ]]; then # 按需创建目录 mkdir -pv "${dDir}${dInfo[Y]}/" && dDirList+=("${dInfo[Y]}") fi if [[ ! -f "${dInfo[p]}" ]]; then # 拷贝到备份目录 cp -v "${nPath}" "${dInfo[p]}" 2>>"${eLog}" && rCode=1 || rCode=4 else if [[ `stat --printf=%s "${dInfo[p]}"` -lt `stat --printf=%s "${nPath}"` ]]; then cp -v "${nPath}" "${dInfo[p]}" 2>>"${eLog}" && rCode=3 || rCode=4 else rCode=2 && echo -ne '-' # 文件存在且比源文件大,跳过 fi fi sed -i "${lNum}s/^${sCode},${sInfo[t]}/${rCode},`date +%F\ %T`/" "${fList}" 2>>"${eLog}" # 更新文件状态和时间 $debug && declare -p sInfo dInfo sInfo=() # 用完清空 dInfo=() # 垃圾回收 done
多媒体
2020-03-03 21:15:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
Photo by Kyle Loftus from Pexels Google宣布开源视频剪裁框架AutoFlip,实现智能化自动裁剪视频。
编译:郑云飞 & Coco Liang
技术审校:郑云飞
原文链接:https://ai.googleblog.com/2020/02/autoflip-open-source-framework-for.html
随着移动设备的进一步普及,越来越多的消费者选择在移动设备上观看视频。 据eMarketer2019年的数据,美国消费者每天平均在移动设备上花费3小时43分钟,比花在看电视上的时间还多了8分钟 ,这也是人们第一次被发现花费在移动设备上的时间多于看电视的时间。
然而,传统的内容生产设备制作的视频大多数是 横屏(landscape) 的,而移动显示设备默认是 竖屏的(portrait) ,这就导致横屏内容在竖屏设备上的播放体验并不是很好。
视频裁剪是解决这个问题的方法之一。然而,人工的视频裁剪是一件非常枯燥、耗时且精细的工作,普通人很难胜任。因此,诞生了许多智能视频裁剪的算法,期望通过算法可以自动、快速地完成优质的视频裁剪。Google AI 13日在官博宣布开源框架AutoFlip,就是实现影片智能化自动剪裁的一个解决方案。
AutoFlip是一个基于MediaPipe框架的智能视频剪裁工具。它可以根据指定的宽高比,对影片内容进行分析,制定最佳裁剪策略,并自动输出相同时长的新视频。

左:原始视频(16:9)。中:使用静态的居中裁剪(9:16)重新构图。右:使用AutoFlip(9:16)重新构图。通过检测感兴趣的目标物,AutoFlip可以避免裁剪掉重要的内容。
其中,MediaPipe是一款由Google Research 开发并开源的多媒体机器学习模型应用框架。目前,YouTube、ARCore、Google Home 以及Nest等,都已经与MediaPipe深度整合。
我们也很幸运地联系到了 MediaPipe 团队,对有关AutoFlip移动端的适用性提出了一些疑问,其中,软件工程师 @jiuqiant 表示,根据自己的经验,由于MediaPipe本身是跨平台框架,因此AutoFlip可以轻松移植到Android和iOS。AutoFlip演示依赖于MediaPipe的对象检测和面部跟踪子图,它们都是Android和iOS上MediaPipe的实时应用程序。因此,AutoFlip在移动平台上也应具有类似的性能。
AutoFlip为智能视频剪裁提供了一套全自动的解决方案,它利用先进的目标检测与追踪技术理解视频内容,同时会检测视频中的场景变化以便分场景进行处理。在每一个场景中,视频分析会先分析场景中的显著性内容,然后通过选择不同的相机模式以及对这些显著性内容在视频中连成的路径进行优化,从而达到场景的裁剪与重构。

如图所示,AutoFlip剪裁影片有三个重要的步骤:镜头边界检测、影片内容分析以及重新取景。
1)镜头边界检测
场景或者镜头是连续的影像序列,不存在任何剪辑。为了侦测镜头变化的发生,AutoFlip会计算每一帧颜色的直方图,并与前一帧进行比较。当直方图在一个历史的窗口中以明显不同于以往的速率变化时,则表示镜头切换。为了对整个场景进行优化,AutoFlip会在得出剪辑策略前缓存整个视频。
2)镜头内容分析
Google利用基于深度学习技术的检测模型在视频帧中找出有趣、突出的内容,这些内容通常包括人和动物。但根据应用程序不同,其他元素也会被检测出来,包括文本和广告 logo、运动中的球和动作等。
左:体育录像中的人物检测。右:两个脸部框(“核心”和“所有”脸部标识)
人脸和物体检测模型通过MediaPipe整合到AutoFlip中,这是在CPU上使用了TensorFlow Lite 。这个架构使得AutoFlip的可扩展性更大,开发者们也因此可以便捷地为不同的使用场景和视频内容添加新的检测算法。
3)重新取景
在确定每一帧上感兴趣的目标物之后,就可以做出如何重新剪裁视频内容的逻辑决策了。AutoFlip会根据物体在镜头中的行为,自动选择静止、平移或追踪等最佳取景策略。其中,追踪模式可以在目标对象在画面内移动时对其进行连续和稳定的跟踪。

如上图所示,第一行是 AutoFlip 根据帧级的边界框追踪到的相机路径,第二行是平滑后的相机路径。左侧是目标对象在画面中移动的场景,需要一个追踪相机路径;右侧是目标物体停留在近乎相同位置的场景,一个固定摄像机即可拍摄在整个场景中全部时长的内容。
AutoFlip 有一个属性图,可以提供最佳效果或自定义需求的剪辑。如果发现剪辑出来的镜头无法覆盖整个影片区域的情况时(例如目标在某一帧视频中显得太大),AutoFlip会自动切换到相对不那么激进的策略上。它会使用信箱效应,在保持原始视频尺寸的同时用黑边模式填充影片,使画面看起来更自然。

随着人们用来观看视频的设备越来越多样化,让任何视频格式都能快速适应不同屏幕比例的能力也显得越发重要。而AutoFlip能够快速地自动剪辑影像,适合在各种设备上播放。
和其它机器学习算法一样,AutoFlip 的性能会随着目标检测等能力的提升而大大加强,尤其是衍生出来的能力,例如采访镜头中的说话人检测或动漫中的动物脸检测等等。
Google称接下来会继续改进AutoFlip, 尤其是针对影片前景文字或图标因为重新取景而被裁掉的情况 。同时,Google也希望 AutoFlip 能进一步融合自然语言处理等技术,从而实现更合理的视频智能剪裁。
References: https://insights.digitalmediasolutions.com/articles/digital-mobile-dominate https://github.com/google/mediapipe/issues/471
多媒体
2020-03-03 14:41:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
这几天疫情原因一直待在家,闲来无事,逛了一下 Archlinux 的 Animation 分类 ,发现一个叫 Animata 的开源项目。
观看其 演示视频 后发现这样的基于顶点和三角的变形动画非常不错,和一般的2D骨骼动画相比,Animata 给予我们的控制更多,除了设置影响范围的半径以外,还能指定受骨骼影响的顶点,从而弥补通过圆指定影响范围所遗留的不足(在关节处常常出现影响范围交叉,造成关节的异常运动,不可避免成为骨骼动画的缺陷,常出现在一根骨骼出现在另一根骨骼的直径范围内时);可以在一张 Sprite 上自己布置顶点自己连接三角形;此外,还有IK机制。
如果能与其他动画项目集成的话前途无量,可惜作者早已不再维护,代码放在 Google Code 上,现在只剩下 Archive,而AUR上这个项目的构建文件也依赖 Google Code 上的 Animata 源码。然而今天,众所周知,Google Code 已经不再提供代码克隆服务。
Step 1:转移仓库
Animata 保存于 SVN 仓库,现在公开的SVN仓库已经少之又少,知名的 Codeplex 已经变为只读,Coding 提供的SVN仓库克隆代码必须要提供密码,SVNBucket 只能创建私有项目,Gitee 上传了一份,尚未测试,SourceForge 不明原因无法注册,显示“System Error 5121225”。内心还是想用上 SourceForge 这个正宗的SVN仓库,找遍全网发现只有 一个人 遇到和我一样的问题,还好评论区提到要用国外邮箱,我就用了 outlook 邮箱,最后注册成功了!马上把 Google Code Archive 上下载到的源码上传到了 这里 。
Step 2:修改构建文件
变量 _svntrunk 改为 svn://svn.code.sf.net/p/animata/svn/trunk/ , pkgver 改为 6 ,第6版是自上传到 SourceForge 以来能够构建成功的第一个版本。只是想用这款软件的人跟着这里的说明做已经可以不看下文了,如果对我后续对代码做的修改感兴趣,或者遇到了问题想知道我对代码做的修改,可以接着看 Step 3。
Step 3:修改过时代码
从 Python 2 到 Python 3
原始的构建脚本 trunk/src/SConscript 使用了 Python 2 print "xxxxxx" 的语法,我尝试用兼容语法发现最后构建还是在这里报错,于是决定放弃 Python 2 语法,使用 Python 3 print("xxxxxx") 的语法。
Animata 类 Drawable 与 系统库 /usr/include/X11/Xlib.h 的 Drawable 类混淆
仅仅修改了构建脚本的语法,构建时会发现如下错误: g++ -o build/animataUI.o -c -Wall -Wno-unknown-pragmas -Wno-long-long -pedantic -pthread -Wno-format -DTIXML_USE_STL -DOSC_HOST_LITTLE_ENDIAN -DANIMATA_MAJOR_VERSION=0 -DANIMATA_MINOR_VERSION=004 -ggdb2 -O0 -DDEBUG=1 -march=x86-64 -mtune=generic -pipe -fstack-protector-strong -fno-plt -D_THREAD_SAFE -D_REENTRANT -D_FILE_OFFSET_BITS=64 -D_LARGEFILE_SOURCE -D_LARGEFILE64_SOURCE -I/usr/include -Ibuild/libs -Isrc/libs -Ibuild/libs/tinyxml -Isrc/libs/tinyxml -Ibuild/libs/oscpack -Isrc/libs/oscpack src/animataUI.cpp In file included from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/Xlib.h:678:2: error: reference to 'Drawable' is ambiguous 678 | Drawable drawable; | ^~~~~~~~ In file included from src/Mesh.h:33, from src/animata.h:45, from src/animataUI.h:9, from src/animataUI.cpp:3: src/Drawable.h:31:7: note: candidates are: 'class Animata::Drawable' 31 | class Drawable | ^~~~~~~~ In file included from /usr/include/X11/Xlib.h:44, from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/X.h:97:13: note: 'typedef XID Drawable' 97 | typedef XID Drawable; | ^~~~~~~~ In file included from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/Xlib.h:691:2: error: reference to 'Drawable' is ambiguous 691 | Drawable drawable; | ^~~~~~~~ In file included from src/Mesh.h:33, from src/animata.h:45, from src/animataUI.h:9, from src/animataUI.cpp:3: src/Drawable.h:31:7: note: candidates are: 'class Animata::Drawable' 31 | class Drawable | ^~~~~~~~ In file included from /usr/include/X11/Xlib.h:44, from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/X.h:97:13: note: 'typedef XID Drawable' 97 | typedef XID Drawable; | ^~~~~~~~ In file included from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/Xlib.h:1458:5: error: reference to 'Drawable' is ambiguous 1458 | Drawable /* d */, | ^~~~~~~~ In file included from src/Mesh.h:33, from src/animata.h:45, from src/animataUI.h:9, from src/animataUI.cpp:3: src/Drawable.h:31:7: note: candidates are: 'class Animata::Drawable' 31 | class Drawable | ^~~~~~~~ In file included from /usr/include/X11/Xlib.h:44, from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/X.h:97:13: note: 'typedef XID Drawable' 97 | typedef XID Drawable; | ^~~~~~~~ In file included from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/Xlib.h:1458:5: error: 'Drawable' has not been declared 1458 | Drawable /* d */, | ^~~~~~~~ /usr/include/X11/Xlib.h:1468:5: error: reference to 'Drawable' is ambiguous 1468 | Drawable /* d */, | ^~~~~~~~ In file included from src/Mesh.h:33, from src/animata.h:45, from src/animataUI.h:9, from src/animataUI.cpp:3: src/Drawable.h:31:7: note: candidates are: 'class Animata::Drawable' 31 | class Drawable | ^~~~~~~~ In file included from /usr/include/X11/Xlib.h:44, from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/X.h:97:13: note: 'typedef XID Drawable' 97 | typedef XID Drawable; | ^~~~~~~~ In file included from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/Xlib.h:1468:5: error: 'Drawable' has not been declared 1468 | Drawable /* d */, | ^~~~~~~~ /usr/include/X11/Xlib.h:1586:5: error: reference to 'Drawable' is ambiguous 1586 | Drawable /* d */, | ^~~~~~~~ In file included from src/Mesh.h:33, from src/animata.h:45, from src/animataUI.h:9, from src/animataUI.cpp:3: src/Drawable.h:31:7: note: candidates are: 'class Animata::Drawable' 31 | class Drawable | ^~~~~~~~ In file included from /usr/include/X11/Xlib.h:44, from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/X.h:97:13: note: 'typedef XID Drawable' 97 | typedef XID Drawable; | ^~~~~~~~ In file included from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/Xlib.h:1586:5: error: 'Drawable' has not been declared 1586 | Drawable /* d */, | ^~~~~~~~ /usr/include/X11/Xlib.h:1599:5: error: reference to 'Drawable' is ambiguous 1599 | Drawable /* d */, | ^~~~~~~~ In file included from src/Mesh.h:33, from src/animata.h:45, from src/animataUI.h:9, from src/animataUI.cpp:3: src/Drawable.h:31:7: note: candidates are: 'class Animata::Drawable' 31 | class Drawable | ^~~~~~~~ In file included from /usr/include/X11/Xlib.h:44, from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/X.h:97:13: note: 'typedef XID Drawable' 97 | typedef XID Drawable; | ^~~~~~~~ In file included from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/Xlib.h:1599:5: error: 'Drawable' has not been declared 1599 | Drawable /* d */, | ^~~~~~~~ /usr/include/X11/Xlib.h:1606:5: error: reference to 'Drawable' is ambiguous 1606 | Drawable /* d */, | ^~~~~~~~ In file included from src/Mesh.h:33, from src/animata.h:45, from src/animataUI.h:9, from src/animataUI.cpp:3: src/Drawable.h:31:7: note: candidates are: 'class Animata::Drawable' 31 | class Drawable | ^~~~~~~~ In file included from /usr/include/X11/Xlib.h:44, from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/X.h:97:13: note: 'typedef XID Drawable' 97 | typedef XID Drawable; | ^~~~~~~~ In file included from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/Xlib.h:1606:5: error: 'Drawable' has not been declared 1606 | Drawable /* d */, | ^~~~~~~~ /usr/include/X11/Xlib.h:1613:5: error: reference to 'Drawable' is ambiguous 1613 | Drawable /* d */, | ^~~~~~~~ In file included from src/Mesh.h:33, from src/animata.h:45, from src/animataUI.h:9, from src/animataUI.cpp:3: src/Drawable.h:31:7: note: candidates are: 'class Animata::Drawable' 31 | class Drawable | ^~~~~~~~ In file included from /usr/include/X11/Xlib.h:44, from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/X.h:97:13: note: 'typedef XID Drawable' 97 | typedef XID Drawable; | ^~~~~~~~ In file included from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/Xlib.h:1613:5: error: 'Drawable' has not been declared 1613 | Drawable /* d */, | ^~~~~~~~ /usr/include/X11/Xlib.h:2186:5: error: reference to 'Drawable' is ambiguous 2186 | Drawable /* src */, | ^~~~~~~~ In file included from src/Mesh.h:33, from src/animata.h:45, from src/animataUI.h:9, from src/animataUI.cpp:3: src/Drawable.h:31:7: note: candidates are: 'class Animata::Drawable' 31 | class Drawable | ^~~~~~~~ In file included from /usr/include/X11/Xlib.h:44, from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/X.h:97:13: note: 'typedef XID Drawable' 97 | typedef XID Drawable; | ^~~~~~~~ In file included from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/Xlib.h:2186:5: error: 'Drawable' has not been declared 2186 | Drawable /* src */, | ^~~~~~~~ /usr/include/X11/Xlib.h:2187:5: error: reference to 'Drawable' is ambiguous 2187 | Drawable /* dest */, | ^~~~~~~~ In file included from src/Mesh.h:33, from src/animata.h:45, from src/animataUI.h:9, from src/animataUI.cpp:3: src/Drawable.h:31:7: note: candidates are: 'class Animata::Drawable' 31 | class Drawable | ^~~~~~~~ In file included from /usr/include/X11/Xlib.h:44, from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/X.h:97:13: note: 'typedef XID Drawable' 97 | typedef XID Drawable; | ^~~~~~~~ In file included from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/Xlib.h:2187:5: error: 'Drawable' has not been declared 2187 | Drawable /* dest */, | ^~~~~~~~ /usr/include/X11/Xlib.h:2206:5: error: reference to 'Drawable' is ambiguous 2206 | Drawable /* src */, | ^~~~~~~~ In file included from src/Mesh.h:33, from src/animata.h:45, from src/animataUI.h:9, from src/animataUI.cpp:3: src/Drawable.h:31:7: note: candidates are: 'class Animata::Drawable' 31 | class Drawable | ^~~~~~~~ In file included from /usr/include/X11/Xlib.h:44, from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/X.h:97:13: note: 'typedef XID Drawable' 97 | typedef XID Drawable; | ^~~~~~~~ In file included from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/Xlib.h:2206:5: error: 'Drawable' has not been declared 2206 | Drawable /* src */, | ^~~~~~~~ /usr/include/X11/Xlib.h:2207:5: error: reference to 'Drawable' is ambiguous 2207 | Drawable /* dest */, | ^~~~~~~~ In file included from src/Mesh.h:33, from src/animata.h:45, from src/animataUI.h:9, from src/animataUI.cpp:3: src/Drawable.h:31:7: note: candidates are: 'class Animata::Drawable' 31 | class Drawable | ^~~~~~~~ In file included from /usr/include/X11/Xlib.h:44, from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/X.h:97:13: note: 'typedef XID Drawable' 97 | typedef XID Drawable; | ^~~~~~~~ In file included from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/Xlib.h:2207:5: error: 'Drawable' has not been declared 2207 | Drawable /* dest */, | ^~~~~~~~ /usr/include/X11/Xlib.h:2304:5: error: reference to 'Drawable' is ambiguous 2304 | Drawable /* d */, | ^~~~~~~~ In file included from src/Mesh.h:33, from src/animata.h:45, from src/animataUI.h:9, from src/animataUI.cpp:3: src/Drawable.h:31:7: note: candidates are: 'class Animata::Drawable' 31 | class Drawable | ^~~~~~~~ In file included from /usr/include/X11/Xlib.h:44, from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/X.h:97:13: note: 'typedef XID Drawable' 97 | typedef XID Drawable; | ^~~~~~~~ In file included from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/Xlib.h:2304:5: error: 'Drawable' has not been declared 2304 | Drawable /* d */, | ^~~~~~~~ /usr/include/X11/Xlib.h:2316:5: error: reference to 'Drawable' is ambiguous 2316 | Drawable /* d */, | ^~~~~~~~ In file included from src/Mesh.h:33, from src/animata.h:45, from src/animataUI.h:9, from src/animataUI.cpp:3: src/Drawable.h:31:7: note: candidates are: 'class Animata::Drawable' 31 | class Drawable | ^~~~~~~~ In file included from /usr/include/X11/Xlib.h:44, from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/X.h:97:13: note: 'typedef XID Drawable' 97 | typedef XID Drawable; | ^~~~~~~~ In file included from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/Xlib.h:2316:5: error: 'Drawable' has not been declared 2316 | Drawable /* d */, | ^~~~~~~~ /usr/include/X11/Xlib.h:2324:5: error: reference to 'Drawable' is ambiguous 2324 | Drawable /* d */, | ^~~~~~~~ In file included from src/Mesh.h:33, from src/animata.h:45, from src/animataUI.h:9, from src/animataUI.cpp:3: src/Drawable.h:31:7: note: candidates are: 'class Animata::Drawable' 31 | class Drawable | ^~~~~~~~ In file included from /usr/include/X11/Xlib.h:44, from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/X.h:97:13: note: 'typedef XID Drawable' 97 | typedef XID Drawable; | ^~~~~~~~ In file included from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/Xlib.h:2324:5: error: 'Drawable' has not been declared 2324 | Drawable /* d */, | ^~~~~~~~ /usr/include/X11/Xlib.h:2334:5: error: reference to 'Drawable' is ambiguous 2334 | Drawable /* d */, | ^~~~~~~~ In file included from src/Mesh.h:33, from src/animata.h:45, from src/animataUI.h:9, from src/animataUI.cpp:3: src/Drawable.h:31:7: note: candidates are: 'class Animata::Drawable' 31 | class Drawable | ^~~~~~~~ In file included from /usr/include/X11/Xlib.h:44, from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/X.h:97:13: note: 'typedef XID Drawable' 97 | typedef XID Drawable; | ^~~~~~~~ In file included from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/Xlib.h:2334:5: error: 'Drawable' has not been declared 2334 | Drawable /* d */, | ^~~~~~~~ /usr/include/X11/Xlib.h:2344:5: error: reference to 'Drawable' is ambiguous 2344 | Drawable /* d */, | ^~~~~~~~ In file included from src/Mesh.h:33, from src/animata.h:45, from src/animataUI.h:9, from src/animataUI.cpp:3: src/Drawable.h:31:7: note: candidates are: 'class Animata::Drawable' 31 | class Drawable | ^~~~~~~~ In file included from /usr/include/X11/Xlib.h:44, from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/X.h:97:13: note: 'typedef XID Drawable' 97 | typedef XID Drawable; | ^~~~~~~~ In file included from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/Xlib.h:2344:5: error: 'Drawable' has not been declared 2344 | Drawable /* d */, | ^~~~~~~~ /usr/include/X11/Xlib.h:2354:5: error: reference to 'Drawable' is ambiguous 2354 | Drawable /* d */, | ^~~~~~~~ In file included from src/Mesh.h:33, from src/animata.h:45, from src/animataUI.h:9, from src/animataUI.cpp:3: src/Drawable.h:31:7: note: candidates are: 'class Animata::Drawable' 31 | class Drawable | ^~~~~~~~ In file included from /usr/include/X11/Xlib.h:44, from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/X.h:97:13: note: 'typedef XID Drawable' 97 | typedef XID Drawable; | ^~~~~~~~ In file included from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/Xlib.h:2354:5: error: 'Drawable' has not been declared 2354 | Drawable /* d */, | ^~~~~~~~ /usr/include/X11/Xlib.h:2363:5: error: reference to 'Drawable' is ambiguous 2363 | Drawable /* d */, | ^~~~~~~~ In file included from src/Mesh.h:33, from src/animata.h:45, from src/animataUI.h:9, from src/animataUI.cpp:3: src/Drawable.h:31:7: note: candidates are: 'class Animata::Drawable' 31 | class Drawable | ^~~~~~~~ In file included from /usr/include/X11/Xlib.h:44, from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/X.h:97:13: note: 'typedef XID Drawable' 97 | typedef XID Drawable; | ^~~~~~~~ In file included from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/Xlib.h:2363:5: error: 'Drawable' has not been declared 2363 | Drawable /* d */, | ^~~~~~~~ /usr/include/X11/Xlib.h:2371:5: error: reference to 'Drawable' is ambiguous 2371 | Drawable /* d */, | ^~~~~~~~ In file included from src/Mesh.h:33, from src/animata.h:45, from src/animataUI.h:9, from src/animataUI.cpp:3: src/Drawable.h:31:7: note: candidates are: 'class Animata::Drawable' 31 | class Drawable | ^~~~~~~~ In file included from /usr/include/X11/Xlib.h:44, from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/X.h:97:13: note: 'typedef XID Drawable' 97 | typedef XID Drawable; | ^~~~~~~~ In file included from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/Xlib.h:2371:5: error: 'Drawable' has not been declared 2371 | Drawable /* d */, | ^~~~~~~~ /usr/include/X11/Xlib.h:2380:5: error: reference to 'Drawable' is ambiguous 2380 | Drawable /* d */, | ^~~~~~~~ In file included from src/Mesh.h:33, from src/animata.h:45, from src/animataUI.h:9, from src/animataUI.cpp:3: src/Drawable.h:31:7: note: candidates are: 'class Animata::Drawable' 31 | class Drawable | ^~~~~~~~ In file included from /usr/include/X11/Xlib.h:44, from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/X.h:97:13: note: 'typedef XID Drawable' 97 | typedef XID Drawable; | ^~~~~~~~ In file included from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/Xlib.h:2380:5: error: 'Drawable' has not been declared 2380 | Drawable /* d */, | ^~~~~~~~ /usr/include/X11/Xlib.h:2390:5: error: reference to 'Drawable' is ambiguous 2390 | Drawable /* d */, | ^~~~~~~~ In file included from src/Mesh.h:33, from src/animata.h:45, from src/animataUI.h:9, from src/animataUI.cpp:3: src/Drawable.h:31:7: note: candidates are: 'class Animata::Drawable' 31 | class Drawable | ^~~~~~~~ In file included from /usr/include/X11/Xlib.h:44, from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/X.h:97:13: note: 'typedef XID Drawable' 97 | typedef XID Drawable; | ^~~~~~~~ In file included from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/Xlib.h:2390:5: error: 'Drawable' has not been declared 2390 | Drawable /* d */, | ^~~~~~~~ /usr/include/X11/Xlib.h:2398:5: error: reference to 'Drawable' is ambiguous 2398 | Drawable /* d */, | ^~~~~~~~ In file included from src/Mesh.h:33, from src/animata.h:45, from src/animataUI.h:9, from src/animataUI.cpp:3: src/Drawable.h:31:7: note: candidates are: 'class Animata::Drawable' 31 | class Drawable | ^~~~~~~~ In file included from /usr/include/X11/Xlib.h:44, from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/X.h:97:13: note: 'typedef XID Drawable' 97 | typedef XID Drawable; | ^~~~~~~~ In file included from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/Xlib.h:2398:5: error: 'Drawable' has not been declared 2398 | Drawable /* d */, | ^~~~~~~~ /usr/include/X11/Xlib.h:2406:5: error: reference to 'Drawable' is ambiguous 2406 | Drawable /* d */, | ^~~~~~~~ In file included from src/Mesh.h:33, from src/animata.h:45, from src/animataUI.h:9, from src/animataUI.cpp:3: src/Drawable.h:31:7: note: candidates are: 'class Animata::Drawable' 31 | class Drawable | ^~~~~~~~ In file included from /usr/include/X11/Xlib.h:44, from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/X.h:97:13: note: 'typedef XID Drawable' 97 | typedef XID Drawable; | ^~~~~~~~ In file included from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/Xlib.h:2406:5: error: 'Drawable' has not been declared 2406 | Drawable /* d */, | ^~~~~~~~ /usr/include/X11/Xlib.h:2416:5: error: reference to 'Drawable' is ambiguous 2416 | Drawable /* d */, | ^~~~~~~~ In file included from src/Mesh.h:33, from src/animata.h:45, from src/animataUI.h:9, from src/animataUI.cpp:3: src/Drawable.h:31:7: note: candidates are: 'class Animata::Drawable' 31 | class Drawable | ^~~~~~~~ In file included from /usr/include/X11/Xlib.h:44, from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/X.h:97:13: note: 'typedef XID Drawable' 97 | typedef XID Drawable; | ^~~~~~~~ In file included from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/Xlib.h:2416:5: error: 'Drawable' has not been declared 2416 | Drawable /* d */, | ^~~~~~~~ /usr/include/X11/Xlib.h:2426:5: error: reference to 'Drawable' is ambiguous 2426 | Drawable /* d */, | ^~~~~~~~ In file included from src/Mesh.h:33, from src/animata.h:45, from src/animataUI.h:9, from src/animataUI.cpp:3: src/Drawable.h:31:7: note: candidates are: 'class Animata::Drawable' 31 | class Drawable | ^~~~~~~~ In file included from /usr/include/X11/Xlib.h:44, from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/X.h:97:13: note: 'typedef XID Drawable' 97 | typedef XID Drawable; | ^~~~~~~~ In file included from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/Xlib.h:2426:5: error: 'Drawable' has not been declared 2426 | Drawable /* d */, | ^~~~~~~~ /usr/include/X11/Xlib.h:2436:5: error: reference to 'Drawable' is ambiguous 2436 | Drawable /* d */, | ^~~~~~~~ In file included from src/Mesh.h:33, from src/animata.h:45, from src/animataUI.h:9, from src/animataUI.cpp:3: src/Drawable.h:31:7: note: candidates are: 'class Animata::Drawable' 31 | class Drawable | ^~~~~~~~ In file included from /usr/include/X11/Xlib.h:44, from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/X.h:97:13: note: 'typedef XID Drawable' 97 | typedef XID Drawable; | ^~~~~~~~ In file included from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/Xlib.h:2436:5: error: 'Drawable' has not been declared 2436 | Drawable /* d */, | ^~~~~~~~ /usr/include/X11/Xlib.h:2461:5: error: reference to 'Drawable' is ambiguous 2461 | Drawable /* d */, | ^~~~~~~~ In file included from src/Mesh.h:33, from src/animata.h:45, from src/animataUI.h:9, from src/animataUI.cpp:3: src/Drawable.h:31:7: note: candidates are: 'class Animata::Drawable' 31 | class Drawable | ^~~~~~~~ In file included from /usr/include/X11/Xlib.h:44, from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/X.h:97:13: note: 'typedef XID Drawable' 97 | typedef XID Drawable; | ^~~~~~~~ In file included from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/Xlib.h:2461:5: error: 'Drawable' has not been declared 2461 | Drawable /* d */, | ^~~~~~~~ /usr/include/X11/Xlib.h:2473:5: error: reference to 'Drawable' is ambiguous 2473 | Drawable /* d */, | ^~~~~~~~ In file included from src/Mesh.h:33, from src/animata.h:45, from src/animataUI.h:9, from src/animataUI.cpp:3: src/Drawable.h:31:7: note: candidates are: 'class Animata::Drawable' 31 | class Drawable | ^~~~~~~~ In file included from /usr/include/X11/Xlib.h:44, from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/X.h:97:13: note: 'typedef XID Drawable' 97 | typedef XID Drawable; | ^~~~~~~~ In file included from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/Xlib.h:2473:5: error: 'Drawable' has not been declared 2473 | Drawable /* d */, | ^~~~~~~~ /usr/include/X11/Xlib.h:2481:5: error: reference to 'Drawable' is ambiguous 2481 | Drawable /* d */, | ^~~~~~~~ In file included from src/Mesh.h:33, from src/animata.h:45, from src/animataUI.h:9, from src/animataUI.cpp:3: src/Drawable.h:31:7: note: candidates are: 'class Animata::Drawable' 31 | class Drawable | ^~~~~~~~ In file included from /usr/include/X11/Xlib.h:44, from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/X.h:97:13: note: 'typedef XID Drawable' 97 | typedef XID Drawable; | ^~~~~~~~ In file included from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/Xlib.h:2481:5: error: 'Drawable' has not been declared 2481 | Drawable /* d */, | ^~~~~~~~ /usr/include/X11/Xlib.h:2491:5: error: reference to 'Drawable' is ambiguous 2491 | Drawable /* d */, | ^~~~~~~~ In file included from src/Mesh.h:33, from src/animata.h:45, from src/animataUI.h:9, from src/animataUI.cpp:3: src/Drawable.h:31:7: note: candidates are: 'class Animata::Drawable' 31 | class Drawable | ^~~~~~~~ In file included from /usr/include/X11/Xlib.h:44, from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/X.h:97:13: note: 'typedef XID Drawable' 97 | typedef XID Drawable; | ^~~~~~~~ In file included from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/Xlib.h:2491:5: error: 'Drawable' has not been declared 2491 | Drawable /* d */, | ^~~~~~~~ /usr/include/X11/Xlib.h:2501:5: error: reference to 'Drawable' is ambiguous 2501 | Drawable /* d */, | ^~~~~~~~ In file included from src/Mesh.h:33, from src/animata.h:45, from src/animataUI.h:9, from src/animataUI.cpp:3: src/Drawable.h:31:7: note: candidates are: 'class Animata::Drawable' 31 | class Drawable | ^~~~~~~~ In file included from /usr/include/X11/Xlib.h:44, from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/X.h:97:13: note: 'typedef XID Drawable' 97 | typedef XID Drawable; | ^~~~~~~~ In file included from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/Xlib.h:2501:5: error: 'Drawable' has not been declared 2501 | Drawable /* d */, | ^~~~~~~~ /usr/include/X11/Xlib.h:2622:5: error: reference to 'Drawable' is ambiguous 2622 | Drawable /* d */, | ^~~~~~~~ In file included from src/Mesh.h:33, from src/animata.h:45, from src/animataUI.h:9, from src/animataUI.cpp:3: src/Drawable.h:31:7: note: candidates are: 'class Animata::Drawable' 31 | class Drawable | ^~~~~~~~ In file included from /usr/include/X11/Xlib.h:44, from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/X.h:97:13: note: 'typedef XID Drawable' 97 | typedef XID Drawable; | ^~~~~~~~ In file included from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/Xlib.h:2622:5: error: 'Drawable' has not been declared 2622 | Drawable /* d */, | ^~~~~~~~ /usr/include/X11/Xlib.h:2905:5: error: reference to 'Drawable' is ambiguous 2905 | Drawable /* d */, | ^~~~~~~~ In file included from src/Mesh.h:33, from src/animata.h:45, from src/animataUI.h:9, from src/animataUI.cpp:3: src/Drawable.h:31:7: note: candidates are: 'class Animata::Drawable' 31 | class Drawable | ^~~~~~~~ In file included from /usr/include/X11/Xlib.h:44, from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/X.h:97:13: note: 'typedef XID Drawable' 97 | typedef XID Drawable; | ^~~~~~~~ In file included from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/Xlib.h:2905:5: error: 'Drawable' has not been declared 2905 | Drawable /* d */, | ^~~~~~~~ /usr/include/X11/Xlib.h:2922:5: error: reference to 'Drawable' is ambiguous 2922 | Drawable /* d */, | ^~~~~~~~ In file included from src/Mesh.h:33, from src/animata.h:45, from src/animataUI.h:9, from src/animataUI.cpp:3: src/Drawable.h:31:7: note: candidates are: 'class Animata::Drawable' 31 | class Drawable | ^~~~~~~~ In file included from /usr/include/X11/Xlib.h:44, from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/X.h:97:13: note: 'typedef XID Drawable' 97 | typedef XID Drawable; | ^~~~~~~~ In file included from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/Xlib.h:2922:5: error: 'Drawable' has not been declared 2922 | Drawable /* d */, | ^~~~~~~~ /usr/include/X11/Xlib.h:2932:5: error: reference to 'Drawable' is ambiguous 2932 | Drawable /* which_screen */, | ^~~~~~~~ In file included from src/Mesh.h:33, from src/animata.h:45, from src/animataUI.h:9, from src/animataUI.cpp:3: src/Drawable.h:31:7: note: candidates are: 'class Animata::Drawable' 31 | class Drawable | ^~~~~~~~ In file included from /usr/include/X11/Xlib.h:44, from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/X.h:97:13: note: 'typedef XID Drawable' 97 | typedef XID Drawable; | ^~~~~~~~ In file included from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/Xlib.h:2932:5: error: 'Drawable' has not been declared 2932 | Drawable /* which_screen */, | ^~~~~~~~ /usr/include/X11/Xlib.h:2941:5: error: reference to 'Drawable' is ambiguous 2941 | Drawable /* which_screen */, | ^~~~~~~~ In file included from src/Mesh.h:33, from src/animata.h:45, from src/animataUI.h:9, from src/animataUI.cpp:3: src/Drawable.h:31:7: note: candidates are: 'class Animata::Drawable' 31 | class Drawable | ^~~~~~~~ In file included from /usr/include/X11/Xlib.h:44, from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/X.h:97:13: note: 'typedef XID Drawable' 97 | typedef XID Drawable; | ^~~~~~~~ In file included from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/Xlib.h:2941:5: error: 'Drawable' has not been declared 2941 | Drawable /* which_screen */, | ^~~~~~~~ /usr/include/X11/Xlib.h:2950:5: error: reference to 'Drawable' is ambiguous 2950 | Drawable /* which_screen */, | ^~~~~~~~ In file included from src/Mesh.h:33, from src/animata.h:45, from src/animataUI.h:9, from src/animataUI.cpp:3: src/Drawable.h:31:7: note: candidates are: 'class Animata::Drawable' 31 | class Drawable | ^~~~~~~~ In file included from /usr/include/X11/Xlib.h:44, from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/X.h:97:13: note: 'typedef XID Drawable' 97 | typedef XID Drawable; | ^~~~~~~~ In file included from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/Xlib.h:2950:5: error: 'Drawable' has not been declared 2950 | Drawable /* which_screen */, | ^~~~~~~~ /usr/include/X11/Xlib.h:3033:5: error: reference to 'Drawable' is ambiguous 3033 | Drawable /* d */, | ^~~~~~~~ In file included from src/Mesh.h:33, from src/animata.h:45, from src/animataUI.h:9, from src/animataUI.cpp:3: src/Drawable.h:31:7: note: candidates are: 'class Animata::Drawable' 31 | class Drawable | ^~~~~~~~ In file included from /usr/include/X11/Xlib.h:44, from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/X.h:97:13: note: 'typedef XID Drawable' 97 | typedef XID Drawable; | ^~~~~~~~ In file included from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/Xlib.h:3033:5: error: 'Drawable' has not been declared 3033 | Drawable /* d */, | ^~~~~~~~ /usr/include/X11/Xlib.h:3732:5: error: reference to 'Drawable' is ambiguous 3732 | Drawable /* d */, | ^~~~~~~~ In file included from src/Mesh.h:33, from src/animata.h:45, from src/animataUI.h:9, from src/animataUI.cpp:3: src/Drawable.h:31:7: note: candidates are: 'class Animata::Drawable' 31 | class Drawable | ^~~~~~~~ In file included from /usr/include/X11/Xlib.h:44, from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/X.h:97:13: note: 'typedef XID Drawable' 97 | typedef XID Drawable; | ^~~~~~~~ In file included from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/Xlib.h:3732:5: error: 'Drawable' has not been declared 3732 | Drawable /* d */, | ^~~~~~~~ /usr/include/X11/Xlib.h:3742:5: error: reference to 'Drawable' is ambiguous 3742 | Drawable /* d */, | ^~~~~~~~ In file included from src/Mesh.h:33, from src/animata.h:45, from src/animataUI.h:9, from src/animataUI.cpp:3: src/Drawable.h:31:7: note: candidates are: 'class Animata::Drawable' 31 | class Drawable | ^~~~~~~~ In file included from /usr/include/X11/Xlib.h:44, from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/X.h:97:13: note: 'typedef XID Drawable' 97 | typedef XID Drawable; | ^~~~~~~~ In file included from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/Xlib.h:3742:5: error: 'Drawable' has not been declared 3742 | Drawable /* d */, | ^~~~~~~~ /usr/include/X11/Xlib.h:3752:5: error: reference to 'Drawable' is ambiguous 3752 | Drawable /* d */, | ^~~~~~~~ In file included from src/Mesh.h:33, from src/animata.h:45, from src/animataUI.h:9, from src/animataUI.cpp:3: src/Drawable.h:31:7: note: candidates are: 'class Animata::Drawable' 31 | class Drawable | ^~~~~~~~ In file included from /usr/include/X11/Xlib.h:44, from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/X.h:97:13: note: 'typedef XID Drawable' 97 | typedef XID Drawable; | ^~~~~~~~ In file included from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/Xlib.h:3752:5: error: 'Drawable' has not been declared 3752 | Drawable /* d */, | ^~~~~~~~ /usr/include/X11/Xlib.h:3762:5: error: reference to 'Drawable' is ambiguous 3762 | Drawable /* d */, | ^~~~~~~~ In file included from src/Mesh.h:33, from src/animata.h:45, from src/animataUI.h:9, from src/animataUI.cpp:3: src/Drawable.h:31:7: note: candidates are: 'class Animata::Drawable' 31 | class Drawable | ^~~~~~~~ In file included from /usr/include/X11/Xlib.h:44, from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/X.h:97:13: note: 'typedef XID Drawable' 97 | typedef XID Drawable; | ^~~~~~~~ In file included from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/Xlib.h:3762:5: error: 'Drawable' has not been declared 3762 | Drawable /* d */, | ^~~~~~~~ /usr/include/X11/Xlib.h:3773:5: error: reference to 'Drawable' is ambiguous 3773 | Drawable /* d */, | ^~~~~~~~ In file included from src/Mesh.h:33, from src/animata.h:45, from src/animataUI.h:9, from src/animataUI.cpp:3: src/Drawable.h:31:7: note: candidates are: 'class Animata::Drawable' 31 | class Drawable | ^~~~~~~~ In file included from /usr/include/X11/Xlib.h:44, from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/X.h:97:13: note: 'typedef XID Drawable' 97 | typedef XID Drawable; | ^~~~~~~~ In file included from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/Xlib.h:3773:5: error: 'Drawable' has not been declared 3773 | Drawable /* d */, | ^~~~~~~~ /usr/include/X11/Xlib.h:3784:5: error: reference to 'Drawable' is ambiguous 3784 | Drawable /* d */, | ^~~~~~~~ In file included from src/Mesh.h:33, from src/animata.h:45, from src/animataUI.h:9, from src/animataUI.cpp:3: src/Drawable.h:31:7: note: candidates are: 'class Animata::Drawable' 31 | class Drawable | ^~~~~~~~ In file included from /usr/include/X11/Xlib.h:44, from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/X.h:97:13: note: 'typedef XID Drawable' 97 | typedef XID Drawable; | ^~~~~~~~ In file included from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/Xlib.h:3784:5: error: 'Drawable' has not been declared 3784 | Drawable /* d */, | ^~~~~~~~ /usr/include/X11/Xlib.h:3795:5: error: reference to 'Drawable' is ambiguous 3795 | Drawable /* d */, | ^~~~~~~~ In file included from src/Mesh.h:33, from src/animata.h:45, from src/animataUI.h:9, from src/animataUI.cpp:3: src/Drawable.h:31:7: note: candidates are: 'class Animata::Drawable' 31 | class Drawable | ^~~~~~~~ In file included from /usr/include/X11/Xlib.h:44, from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/X.h:97:13: note: 'typedef XID Drawable' 97 | typedef XID Drawable; | ^~~~~~~~ In file included from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/Xlib.h:3795:5: error: 'Drawable' has not been declared 3795 | Drawable /* d */, | ^~~~~~~~ /usr/include/X11/Xlib.h:3806:5: error: reference to 'Drawable' is ambiguous 3806 | Drawable /* d */, | ^~~~~~~~ In file included from src/Mesh.h:33, from src/animata.h:45, from src/animataUI.h:9, from src/animataUI.cpp:3: src/Drawable.h:31:7: note: candidates are: 'class Animata::Drawable' 31 | class Drawable | ^~~~~~~~ In file included from /usr/include/X11/Xlib.h:44, from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/X.h:97:13: note: 'typedef XID Drawable' 97 | typedef XID Drawable; | ^~~~~~~~ In file included from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/Xlib.h:3806:5: error: 'Drawable' has not been declared 3806 | Drawable /* d */, | ^~~~~~~~ /usr/include/X11/Xlib.h:3817:5: error: reference to 'Drawable' is ambiguous 3817 | Drawable /* d */, | ^~~~~~~~ In file included from src/Mesh.h:33, from src/animata.h:45, from src/animataUI.h:9, from src/animataUI.cpp:3: src/Drawable.h:31:7: note: candidates are: 'class Animata::Drawable' 31 | class Drawable | ^~~~~~~~ In file included from /usr/include/X11/Xlib.h:44, from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/X.h:97:13: note: 'typedef XID Drawable' 97 | typedef XID Drawable; | ^~~~~~~~ In file included from /usr/include/FL/x.H:37, from /usr/include/FL/fl_draw.H:27, from src/animataUI.cpp:1555: /usr/include/X11/Xlib.h:3817:5: error: 'Drawable' has not been declared 3817 | Drawable /* d */, | ^~~~~~~~ scons: *** [build/animataUI.o] Error 1 scons: building terminated because of errors.
简而言之, src/animataUI.cpp 编译失败。
分析之后发现 Animata 类 Drawable 与 系统库 /usr/include/X11/Xlib.h 的 Drawable 类混淆了,遂将 Animata 类 Drawable 改为 ADrawable,相应地修改其他源码,最后构建成功。
这是 Garlic Player 之后第二个在遇到构建失败问题后依靠自己的分析判断,做出行动以后构建成功的 Linux 软件。Garlic Player 只是缺了 vlc 依赖,这个直接修改了源码并成功了,为构建积累了宝贵的经验。
后记 中国加油!武汉加油! 自从在评论区声明自己构建成功以后,现在原提交者 ianux 已经将维护的重担交给了我,受宠若惊,遂上传了自己修改过的 PKGBUILD,经再次测试,构建成功 现在做开源动画非常强劲的项目是 Morevna Project ,已经向他们提交了接收 Animata 这个孤儿包的 申请 ,以期借鉴并完善 Synfig Studio 的骨骼动画系统。如果大家支持这个决定,还请多多到 Issues 上面支持我!
多媒体
2020-03-03 14:33:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
写在前面
因为忙,2019年我博客停更了一年,2020开局受肺炎影响,大家都推迟上班在家呆着,这时有网络上的朋友加我QQ问我,我才意识到这个系列我写得晦涩难懂,不适合入门学习。但我回头来看看这个系列,对我个人还是蛮有意义的,毕竟能深入分析QtLocation源码的文章不多,等有些朋友真正要基于QtLocation做魔改的时候,再来看我这个系列的文章就合适了。如果只是要跑通一些demo,马上看到上手效果的朋友,可以进入 传送门 ,这是找我问问题的那位朋友告诉我的。
2018年下半年,我们公司想把地面站软件从.NET平台移植到Qt平台,从那时开始我就负责为Qt版地面站制定地图技术方案,以及完成相关功能的移植和开发。 虽然 QtLocation 提供了许多地理服务插件,源码都在geoservices目录里,但我的工作中还是需要使用其他LBS插件,比如不翻墙不加偏的谷歌地图 。 不翻墙不加偏的谷歌地图不能讲,那我就讲讲QtLocation的Mapping服务以及瓦片缓存与获取机制吧!
QtLocation之Mapping服务机制
我在前面已经提到过了,导入 Qt Location 模块,实例化一个QML类型中的 Map 对象,只要为它配置一个具体的LBS插件,就可以在前端显示并浏览地图了(下面是Qt官方例子 minimal_map的完整代码 ): import QtQuick 2.0 import QtQuick.Window 2.0 import QtLocation 5.6 import QtPositioning 5.6 Window { width: 512 height: 512 visible: true Plugin { id: mapPlugin name: "osm" // "mapboxgl", "esri", ... // specify plugin parameters if necessary // PluginParameter { // name: // value: // } } Map { anchors.fill: parent plugin: mapPlugin center: QtPositioning.coordinate(59.91, 10.75) // Oslo zoomLevel: 14 } }
上述代码以 OpenStreetMap 插件为例:当QML引擎加载该QML脚本,在前端实例化这个 Map 对象时,本质上是在后端实例化出了 Map 的宿主对象,即QDeclarativeGeoMap的实例,前端 Map 对象的所有行为和能力都靠QDeclarativeGeoMap的实例来实现;当前端脚本执行到plugin: Plugin { name: "osm" }语句时,后端实际上在调用QDeclarativeGeoMap对象的setPlugin方法,设置成功后会触发LBS插件的attached信号,进而调用QDeclarativeGeoMap的pluginReady槽,进而调用mappingManagerInitialized,进而通过QGeoMappingManager对象创建 QGeoMap 的实例;而在QGeoMappingManager对象内部包装着一个 QGeoMappingManagerEngine 对象, QGeoMap 的实例实际上是由 QGeoMappingManagerEngine 对象创建的; QGeoMap 的实例创建后,分配给QDeclarativeGeoMap的一个类型为QPointer< QGeoMap >的私有成员,然后调用 QDeclarativeGeoMap 对象的updatePaintNode方法,刷新渲染树节点,从而实现 QtLocation模块 渲染地图的前-后端机制。这里的QGeoMap是由 QGeoTiledMap 继承并具体实现的, QGeoTiledMap 提供了瓦片地图的渲染机制。
QGeoMappingManagerEngine 就是提供Mapping服务的核心类型,它及其派生类(如 QGeoTiledMappingManagerEngine )组织起了 QLocale 、QList< QGeoMapType >、 QAbstractGeoTileCache 、 QGeoTileFetcher 和 QGeoCameraCapabilities 等类型的成员,并在构造函数及其派生类的构造函数里面对它们进行实例化。
这些成员的类型都顾名思义: QLocale 提供本地化功能;QList< QGeoMapType >容器里装着当前插件所支持的某些地图类型(以 OpenStreetMap 插件为例,其提供StreetMap、SatelliteMapDay、CycleMap、TransitMap、TerrainMap和PedestrianMap等类型的地图); QAbstractGeoTileCache 是提供瓦片缓存机制的抽象类型,通过继承它,QtLocation自己实现了一个 QGeoFileTileCache 类型; QGeoTileFetcher 提供从网络服务获取瓦片的方法; QGeoCameraCapabilities 记录了该Mapping服务能够提供哪些地图浏览功能(以 OpenStreetMap 插件为例,其MaximumZoomLevel = 19, SupportsBearing = true, SupportsTilting = true, MinimumTilt = 0, MaximumTilt = 80, MinimumFieldOfView = 20, MaximumFieldOfView = 120)。
这里顺带提一句,QtLocation现在主要是一个二维的瓦片地图插件框架,但是它的二维地图底层从一开始就是支持倾斜浏览和视场角调节的,这一点还是很前卫的,似乎怀揣一颗兼容三维地图的野心哈!我在前面的章节也讲了,它的地图层次模型和渲染机制其实和谷歌地球很像,本质上都是基于文档对象模型(Document Object Model,DOM)的Web流派。
QtLocation之LBS静态插件扩展
那么,我要 使用其他的LBS插件,比如 谷歌地图 和 必应地图 ,或者自己实现的 LBS插件 ,我该如何扩展出这个功能呢(直白地讲,就是把plugin: Plugin { name: "osm" }的name换成非官方的LBS名称,需要做什么QtLocation才认可呢)?这里涉及到协变和逆变的东西,就不展开讲了。总之,我们要明确,前端只有 Map 这个QML类型可用,最终要在LBS插件的帮助下实现多态。
Qt几乎就是为扩展而生的,《 How to Create Qt Plugins 》中详细介绍了高级(平台级)插件扩展方法和低级(应用程序级)插件扩展方法。地面站这种应用场景下,我当然更希望我的LBS插件跟着我的应用程序走,而且能 将其编译为单独提供的动态库,并在运行时自动进行检测和加载 。所以,选用 Qt静态插件 来实现, 这一招 是从 QGroundControl 学的。
以 谷歌地图 LBS插件 为例,对其QGeoServiceProviderFactoryGooglemaps加以改造(省略原有部分): // qgeoserviceproviderplugingooglemaps.h : ... GEOSERVICESSHARED_EXPORT const QT_PREPEND_NAMESPACE(QStaticPlugin) qt_static_plugin_QGeoServiceProviderFactoryGooglemaps(); class GEOSERVICESSHARED_EXPORT QGeoServiceProviderFactoryGooglemaps: public QObject, public QGeoServiceProviderFactory { ... }; // qgeoserviceproviderplugingooglemaps.cpp : ... // Set google maps service provider as qt static plugin------------------------------------------ Q_EXTERN_C Q_DECL_EXPORT const char *qt_plugin_query_metadata(); Q_EXTERN_C Q_DECL_EXPORT QT_PREPEND_NAMESPACE(QObject) *qt_plugin_instance(); const QT_PREPEND_NAMESPACE(QStaticPlugin) qt_static_plugin_QGeoServiceProviderFactoryGooglemaps() { QT_PREPEND_NAMESPACE(QStaticPlugin) plugin = { qt_plugin_instance, qt_plugin_query_metadata}; return plugin; } //----------------------------------------------------------------------------------------------- ...

QtLocation之瓦片缓存与获取机制
QtLocation的瓦片缓存机制主要由 QGeoFileTileCache 及其派生类型实现,提供了QGeoTileTexture、QGeoCachedTileDisk和QGeoCachedTileMemory和等三种类型的缓存。其实后两种类型的缓存都存在于内存中,它们主要的差别是:memory的瓦片数据是用QByteArray类型表示;而texture的瓦片数据已经表示成QImage这种图像类型,可以直接提供给地图引擎进行渲染。disk的瓦片数据就表示为文件路径和格式了。 // disk class QGeoCachedTileDisk { public: ~QGeoCachedTileDisk(); QGeoTileSpec spec; QString filename; QString format; QGeoFileTileCache *cache; }; // memory class QGeoCachedTileMemory { public: ~QGeoCachedTileMemory() { if (cache) cache->evictFromMemoryCache(this); } QGeoTileSpec spec; QGeoFileTileCache *cache; QByteArray bytes; QString format; }; // texture class Q_LOCATION_PRIVATE_EXPORT QGeoTileTexture { public: QGeoTileTexture(); ~QGeoTileTexture(); QGeoTileSpec spec; QImage image; bool textureBound; };
每次需要瓦片(比如渲染瓦片地图)时,就需要调用 QGeoTiledMappingManagerEngine 的updateTileRequests方法,瓦片地图引擎会优先从 QGeoFileTileCache 那里get瓦片,get不到再整理好剩余所需瓦片的信息,一起扔给 QGeoTileFetcher 让其在后台从网络上获取。
QGeoFileTileCache 定义如下所示(省略非核心部分): class Q_LOCATION_PRIVATE_EXPORT QGeoFileTileCache : public QAbstractGeoTileCache { Q_OBJECT public: QGeoFileTileCache(const QString &directory = QString(), QObject *parent = 0); ~QGeoFileTileCache(); ... QSharedPointer get(const QGeoTileSpec &spec) override; // can be called without a specific tileCache pointer static void evictFromDiskCache(QGeoCachedTileDisk *td); static void evictFromMemoryCache(QGeoCachedTileMemory *tm); void insert(const QGeoTileSpec &spec, const QByteArray &bytes, const QString &format, QAbstractGeoTileCache::CacheAreas areas = QAbstractGeoTileCache::AllCaches) override; ... protected: ... QString directory() const; QSharedPointer addToDiskCache(const QGeoTileSpec &spec, const QString &filename); bool addToDiskCache(const QGeoTileSpec &spec, const QString &filename, const QByteArray &bytes); void addToMemoryCache(const QGeoTileSpec &spec, const QByteArray &bytes, const QString &format); QSharedPointer addToTextureCache(const QGeoTileSpec &spec, const QImage &image); QSharedPointer getFromMemory(const QGeoTileSpec &spec); QSharedPointer getFromDisk(const QGeoTileSpec &spec); virtual bool isTileBogus(const QByteArray &bytes) const; virtual QString tileSpecToFilename(const QGeoTileSpec &spec, const QString &format, const QString &directory) const; virtual QGeoTileSpec filenameToTileSpec(const QString &filename) const; QCache3Q diskCache_; QCache3Q memoryCache_; QCache3Q textureCache_; ... };
从上述代码可以看出, QGeoFileTileCache 内部以QGeoTileSpec为键(Key,或者说索引),对三种类型瓦片都做了3Q缓存(QCache3Q)。QGeoTileSpec类型就跟其他瓦片地图框架(比如 Brutile )的TileInfo类型一样,记录着该瓦片的行列号(x,y)和缩放级别(Zoom Level)等信息。 QGeoFileTileCache 的get逻辑也很简单,先找texture,再找memory,最后找disk,但最终提交给 QGeoTiledMappingManagerEngine 的都是QSharedPointer
知道了QtLocation的瓦片缓存和获取机制后,就可以想办法增加自定义的缓存管理功能了。 官方的LBS插件都可以通过“[xxx]. mapping.cache.directory ”参数来配置文件缓存目录,这里的 cache.directory中存放的就是 QGeoCachedTileDisk索引的缓存文件。
我们在实现离线缓存功能的时候,最好不要去直接共用 cache.directory 目录。 因为这样既容易破坏 QGeoFileTileCache 自身的3Q缓存机制,又受到3Q缓存机制的限制(该缓存机制有容量限制,会将长时间未使用的瓦片清理掉,而离线缓存中的瓦片我们往往希望不被自动清理)。可以参考 Open Street Map Plugin ,通过“[xxx].mapping.offline.directory”插件参数,另起一个离线缓存目录来实现它。例如继承QGeoFileTileCache实现的QGeoFileTileCacheOsm派生类里,通过重写get方法把离线缓存目录中的瓦片也使用起来: // qgeofiletilecacheosm.h : QSharedPointer get(const QGeoTileSpec &spec) override; // qgeofiletilecacheosm.cpp : QSharedPointer QGeoFileTileCacheOsm::get(const QGeoTileSpec &spec) { QSharedPointer tt = getFromMemory(spec); if (tt) return tt; if ((tt = getFromOfflineStorage(spec))) return tt; return getFromDisk(spec); }
离线缓存功能的具体实现,留到下一章再继续讲。
多媒体
2020-03-02 01:24:00
「深度学习福利」大神带你进阶工程师,立即查看>>> 昨天整理照片的时候用到一个新工具 Exiv2 ,配合脚本: 按照片的拍摄时间重命名 的功能。
为什么要重命名?列出一些照片的文件名,大家感受一下: 1360654147987.jpeg -1998496702.jpeg 2008-12-27-0.bmp 2013-07-13 21.55.00-1.jpg 2013_12_24_18_22_27.jpg 20140217_195036_Android.jpg 2014_07_13_23_34_15.jpg 2015-03-22 155333(1).jpg 2017-11-24 091519.jpg 20180328210442441.jpg 2Q1A3602.JPG 5D3_6064.jpg 6734108497056027763.JPG DSC_1718.JPG DSCF1657.jpg FC047988-8BBA-4DA4-97FE-CAC7E709DC87-8452-00000D1BB3131E23_tmp_1.jpg hdImg_c3511400.jpg IMG_2008.HEIC IMG_20120520_131912_compress.jpg IMG_20141018_154658_1_1538025858307.jpg IMG_20141109_132144_1.jpg IMG20141223214713.jpg IMG_20150322_160154_BURST2.jpg IMG20160704222659.jpg IMG_20170422_005407_HDR.jpg IMG_20171006_131836_HHT.jpg IMG_2611.PNG IMG_4775_3.JPG IMG_9130.CR2 microMsg_1567686722211.jpeg mmexport1573908606434.jpg moodSyncWeChat.jpeg MYXJ_20160318142113_fast.jpg navi_photo_18666952619.jpg PA020136.JPG PANO_20141112_173323.jpg SAM_0297.jpg Screenshot_2012-03-28-21-11-41.png VID_20140419_131717.jpg wx_camera_1572670076056.jpg XiaoMi_IMG_20140529_203548.jpg ZMS_7688.jpg 图像001.PNG 照片 618.JPG 今天继续优化脚本时发现 Exiv2 本身就自带了 “ 按拍摄时间重命名 ” 的功能:
exiv2 -v -r'%Y%m%d-%H%M%S' -f IMG_13331.jpg # File 1/1: IMG_13331.jpg # Renaming file to ./20180916-133120.jpg 支持批量操作,且支持名字重复加后缀的功能。 exiv2 -v -r'%Y%m%d-%H%M%S' -F *.{jpg,jpeg,png,cr2,heic} 手机拍摄的照片都包含了拍摄时间。截图、处理后的照片、古老的照片,没有拍摄时间的用 Exiv2 追加上: exiv2 -pa IMG_0516.jpg exiv2 -v -M"set Exif.Photo.DateTimeOriginal 2009:10:24 22:33:33" IMG_0516.jpg exiv2 -v -M"set Exif.Photo.UserComment From:~higkoo/IMG_0516.jpg" IMG_0516.jpg 爽快,把文件统统来个改名,一键搞定: find /volume1/photo/MobileDevice ! -path "*@eaDir*" -type f -exec exiv2 -v -r'%Y%m%d-%H%M%S' -f {} \; 然后再把重命名成功的文件按年份归档起来,补个简单的 脚本 : #!/bin/bash # Copy photos to the format path sDir="${1:-/volume1/photo/MobileDevice/}" # 需要扫描的源目录 dDir="${2:-/volume1/homes/higkoo/XiaoMi/Image/}" # 归档目录 mDepth="${3:-2}" # 默认只进2层目录 readonly debug=false # 调试开关 readonly iLog="${dDir}FileList.csv" # 扫描日志 readonly eLog="${dDir}Error.log" declare -r mCode=([0]="Noneed" [1]="Created" [2]="Exsit" [3]="Overwrite" [4]="Failed" [5]="Error" [6]="Unknow") # 错误码定义 declare -A dPathInfo=([Y]=2000 [m]=11 [d]=22 [H]=11 [M]=22 [S]=33 [p]="/dev/null") # 解析文件名信息 declare -a dDirList=() # 减少文件操作,用临时内存操作替换 rCode=6 # 错误码 function AddLog(){ echo "${rCode:-6},$(date +%F\ %T),${sPath},${dPathInfo[p]}" >>"${iLog}" 2>>"${eLog}" && return 0 || return 1 } function ParsePath(){ dPathInfo=() sfName="${sPath##*/}" # 文件名(带后缀) spName="${sfName%.*}" # 文件名(无后缀) stStr="${spName#*-}" # 时分秒串 sExt="${sfName##*.}" # 后缀名 [ "${sfName:8:1}" = "-" -a ${#spName} -eq 15 ] || return 1 # 用文件名判断合法性 [[ "${sExt,,}" = "jpeg" ]] && sExt='jpg' # 将jpeg后缀改为jpg read dPathInfo[Y] dPathInfo[m] dPathInfo[d] dPathInfo[H] dPathInfo[M] dPathInfo[S] < <(date -d "${spName%-*} ${stStr:0:2}:${stStr:2:2}:${stStr:4:2}" +"%Y %m %d %H %M %S") 2>>"${eLog}" || rCode=5 dPathInfo[p]="${dDir}${dPathInfo[Y]}/${spName}.${sExt,,}" && return 0 # 目标全路径 、文件后缀统一转成小写 return 1 } touch "${iLog}" "${eLog}" find "${sDir}" -maxdepth "${mDepth}" ! -path "*@eaDir*" -type f -iname "*.jpg" -o -iname "*.jpeg" -o -iname "*.heic" -o -iname "*.cr2" | while read sPath; do unset sfName spName stStr sExt # 避免循环过程遇到错误时信息污染 $debug && sPath='/volume1/photo/MobileDevice/iPhone/2018-09-16 105911.jpg' grep -q "${sPath}" "${iLog}" 2>>"${eLog}" if [[ $? -eq 0 ]]; then echo -ne '.' # 已扫描过,跳过 continue fi ParsePath if [[ $? -ne 0 ]]; then rCode=0 && AddLog echo -ne '_' # 非格式化的文件名,跳过 $debug && echo "${sPath}" continue fi $debug && declare -p dPathInfo dDirList if [[ ! ${dDirList[*]} =~ (^|[[:space:]])"${dPathInfo[Y]}"($|[[:space:]]) ]]; then # 判断目录是否已经创建过 mkdir -pv "${dDir}${dPathInfo[Y]}/" && dDirList+=("${dPathInfo[Y]}") fi if [[ ! -f "${dPathInfo[p]}" ]]; then cp -v "${sPath}" "${dPathInfo[p]}" 2>>"${eLog}" && rCode=1 || rCode=4 else if [[ `stat --printf=%s "${dPathInfo[p]}"` -lt `stat --printf=%s "${sPath}"` ]]; then cp -v "${sPath}" "${dPathInfo[p]}" 2>>"${eLog}" && rCode=3 || rCode=4 else rCode=2 && echo -ne '-' # 文件存在且比源文件大,跳过 fi fi AddLog # 确保处理过的都按标准格式记录日志 done
端杯水,休息一会~
经得起检验的脚本才是好脚本,处理成功 37503 张照片、 0 错误。
多媒体
2020-03-01 22:44:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
写在前面 现代生活离不开手机,照片也是日积月累。 至从在操作 iPhone4 的 iCloud 不慎丢失照片之后,痛定思痛。 后面的日子一直备份照片、多地备份。时间久了,照片越来越多,整理起来非常头疼。
遇到的问题 积攒了10多年的照片,文件数量已接近5万、体积也过250G了。 相同照片不同分辨率的版本。 截屏图片、非手机拍照的照片。 手动整理,遇到量大,无所适从。
我的思考 以前只管存,没有花时间整理。现在疫情封闭,是时候整理它了。 买了群晖存储,把过往照片全存进去。 摸索了一些方式,最终决定写个程序去整理。
整理思路 只保留带 Exif 信息的照片,手机/数码相机拍的原图必定包含这些信息。 按年来分目录存放,这样目录不会太多,单目录文件也不太多。 照片按统一的时间格式重命名。
开干 经过几轮编写、调试、验证,脚本新鲜出炉:
话不多说,简短源码贴出来: #!/bin/bash # Move photo to a format path sDir="${1:-/volume1/photo/All-In/}" dDir="${2:-/volume1/photo/Image/}" mDepth="${3:-2}" iLog="${dDir}move.csv" eLog="${dDir}move.err" declare -A mCode=(["0"]="Created" ["1"]="Exsit" ["2"]="Overwrite" ["3"]="Noneed" ["4"]="Unknow" ["5"]="Failed") function AddLog(){ echo "${rCode:-4},$(date +%F\ %T),${sPath},${dPath},${fTime}" >>"${iLog}" 2>>"${eLog}" return "${rCode}" } touch "${iLog}" "${eLog}" find "${sDir}" -maxdepth "${mDepth}" ! -path "*@eaDir*" -type f -iname "*.jpg" -o -iname "*.jpeg" -o iname "*.cr2" | while read sPath; do unset "rCode" "dPath" "oTime" "fTime" grep -q "${sPath}" "${iLog}" && continue || oTime=`exiv2 -q -g Exif.Photo.DateTimeOriginal -P v "${sPath}" 2>>"${eLog}"` if [ -z "${oTime}" ]; then rCode=3 && AddLog continue fi fTime=$(echo "${oTime}" | sed 's/:/-/;s/:/-/') dPath=$(date -d "${fTime}" +"${dDir}%Y/%Y%m%d-%H%M%S.jpg") if [ -f "${dPath}" ]; then if [ `stat --printf=%s "${dPath}"` -lt `stat --printf=%s "${sPath}"` ]; then cp -v "${sPath}" "${dPath}" 2>>"${eLog}" && rCode=2 || rCode=5 else rCode=1 fi else cp -v "${sPath}" "${dPath}" 2>>"${eLog}" && rCode=0 || rCode=5 fi AddLog done
验证效果 数据无价,读不懂脚本的朋友不建议尝试。开启群晖的ssh-server,连上去、跑起来!晒下执行现场:
解决了我的大问题 自动筛选原图 自动去重(保留最大的) 生成图片信息库 以后再用不用担心图片多,存也不是、丢也不是了。
./Move.sh "/volume1/photo/MixTime/" "/volume1/photo/Image/" 1
多媒体
2020-02-29 16:44:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
本文整理自《CNCF x Alibaba 云原生技术公开课》第 23 讲,点击“阅读原文”直达课程页面。
关注“阿里巴巴云原生”公众号,回复关键词 “入门” ,即可下载从零入门 K8s 系列文章 PPT。
导读 :在 Kubernetes 里面, API 编程范式也就是 Custom Resources Definition(CRD)。我们常讲的 CRD,其实指的就是用户自定义资源。为什么会存在用户自定义资源问题呢?本文将会从其需求来源出发,对此概念进行逐步深入的讲解。
一、需求来源
首先我们先来看一下 API 编程范式的需求来源。
在 Kubernetes 里面, API 编程范式也就是 Custom Resources Definition(CRD)。我们常讲的 CRD,其实指的就是用户自定义资源。
为什么会有用户自定义资源问题呢?
随着 Kubernetes 使用的越来越多,用户自定义资源的需求也会越来越多。而 Kubernetes 提供的聚合各个子资源的功能,已经不能满足日益增长的广泛需求了。用户希望提供一种用户自定义的资源,把各个子资源全部聚合起来。但 Kubernetes 原生资源的扩展和使用比较复杂,因此诞生了用户自定义资源这么一个功能。
二、用例解读
CRD 的一个实例
我们首先具体地介绍一下 CRD 是什么。
CRD 功能是在 Kubernetes 1.7 版本被引入的,用户可以根据自己的需求添加自定义的 Kubernetes 对象资源。值得注意的是,这里用户自己添加的 Kubernetes 对象资源都是 native 的、都是一等公民,和 Kubernetes 中自带的、原生的那些 Pod、Deployment 是同样的对象资源。在 Kubernetes 的 API Server 看来,它们都是存在于 etcd 中的一等资源。
同时,自定义资源和原生内置的资源一样,都可以用 kubectl 来去创建、查看,也享有 RBAC、安全功能。用户可以开发自定义控制器来感知或者操作自定义资源的变化。
下面我们来看一个简单的 CRD 实例。下图是一个 CRD 的定义。
首先最上面的 apiVersion 就是指 CRD 的一个 apiVersion 声明,声明它是一个 CRD 的需求或者说定义的 Schema。
kind 就是 CustomResourcesDefinition,指 CRD。name 是一个用户自定义资源中自己自定义的一个名字。一般我们建议使用“顶级域名.xxx.APIGroup”这样的格式,比如这里就是 foos.samplecontroller.k8s.io。
spec 用于指定该 CRD 的 group、version。比如在创建 Pod 或者 Deployment 时,它的 group 可能为 apps/v1 或者 apps/v1beta1 之类,这里我们也同样需要去定义 CRD 的 group。 图中的 group 为 samplecontroller.k8s.io; verison 为 v1alpha1; names 指的是它的 kind 是什么,比如 Deployment 的 kind 就是 Deployment,Pod 的 kind 就是 Pod,这里的 kind 被定义为了 Foo; plural 字段就是一个昵称,比如当一些字段或者一些资源的名字比较长时,可以用该字段自定义一些昵称来简化它的长度; scope 字段表明该 CRD 是否被命名空间管理。比如 ClusterRoleBinding 就是 Cluster 级别的。再比如 Pod、Deployment 可以被创建到不同的命名空间里,那么它们的 scope 就是 Namespaced 的。这里的 CRD 就是 Namespaced 的。
下图就是上图所定义的 CRD 的一个实例。
它的 apiVersion 就是我们刚才所定义的 samplecontroller.k8s.io/v1alpha1; kind 就是 Foo; metadata 的 name 就是我们这个例子的名字; 这个实例中 spec 字段其实没有在 CRD 的 Schema 中定义,我们可以在 spec 中根据自己的需求来写一写,格式就是 key:value 这种格式,比如图中的 deploymentName: example-foo, replicas: 1。当然我们也可以去做一些检验或者状态资源去定义 spec 中到底包含什么。
带有校验的 CRD
我们来看一个包含校验的 CRD 定义:
可以看到这个定义更加复杂了,validation 之前的字段我们就不再赘述了,单独看校验这一段。
它首先是一个 openAPIV3Schema 的定义,spec 中则定义了有哪些资源,以 replicas 为例,这里将 replicas 定义为一个 integer 的资源,最小值为 1,最大值是 10。那么,当我们再次使用这个 CRD 的时候,如果我们给出的 replicas 不是 int 值,或者去写一个 -1,或者大于 10 的值,这个 CRD 对象就不会被提交到 API Server,API Server 会直接报错,告诉你不满足所定义的参数条件。
带有状态字段的 CRD
再来看一下带有状态字段的 CRD 定义。
我们在使用一些 Deployment 或 Pod 的时候,部署完成之后可能要去查看当前部署的状态、是否更新等等。这些都是通过增加状态字段来实现的。另外,Kubernetes 在 1.12 版本之前,还没有状态字段。
状态实际上是一个自定义资源的子资源,它的好处在于,对该字段的更新并不会触发 Deployment 或 Pod 的重新部署。我们知道对于某些 Deployment 和 Pod,只要修改了某些 spec,它就会重新创建一个新的 Deployment 或者 Pod 出来。但是状态资源并不会被重新创建,它只是用来回应当前 Pod 的整个状态。上图中的 CRD 声明中它的子资源的状态非常简单,就是一个 key:value 的格式。在 "{}" 里写什么,都是自定义的。
以一个 Deployment 的状态字段为例,它包含 availableReplicas、当前的状态(比如更新到第几个版本了、上一个版本是什么时候)等等这些信息。在用户自定义 CRD 的时候,也可以进行一些复杂的操作来告诉别的用户它当前的状态如何。
三、操作演示
下面我们来具体演示一下 CRD。
我们这里有两个资源:crd.yaml 和 example-foo.yaml。
首先创建一下这个 CRD 的 Schema 让我们的 Kubernetes Server 知道该 CRD 到底是什么样的。创建的方式非常简单,就是 "kuberctl create -f crd.yaml"。
通过 "kuberctl get crd" 可以看到刚才的 CRD 已经被创建成功了。
这个时候我们就可以去创建对应的资源 "kuberctl create -f example-foo.yaml":
下面来看一下它里面到底有什么东西 "kubectl get foo example-foo -o yaml" :
可以看到它是一个 Foo 的资源,spec 就是我们刚才所定义的,被选中的部分是基本上所有的 Kubernetes 的 metadata 资源中都会有的。因此,创建该资源和我们正常创建一个 Pod 的区别并不大,但是这个资源不是一个 Pod,也不是 Kubernetes 本身内置的资源,这就是一个我们自己创建的资源。从使用方式和使用体验上来说,和 Kubernetes 内置资源的使用几乎一致。
四、架构设计
控制器概览
只定义一个 CRD 其实没有什么作用,它只会被 API Server 简单地计入到 etcd 中。如何依据这个 CRD 定义的资源和 Schema 来做一些复杂的操作,则是由 Controller,也就是控制器来实现的。

Controller 其实是 Kubernetes 提供的一种可插拔式的方法来扩展或者控制声明式的 Kubernetes 资源。它是 Kubernetes 的大脑,负责大部分资源的控制操作。以 Deployment 为例,它就是通过 kube-controller-manager 来部署的。
比如说声明一个 Deployment 有 replicas、有 2 个 Pod,那么 kube-controller-manager 在观察 etcd 时接收到了该请求之后,就会去创建两个对应的 Pod 的副本,并且它会去实时地观察着这些 Pod 的状态,如果这些 Pod 发生变化了、回滚了、失败了、重启了等等,它都会去做一些对应的操作。
所以 Controller 才是控制整个 Kubernetes 资源最终表现出来的状态的大脑。
用户声明完成 CRD 之后,也需要创建一个控制器来完成对应的目标。比如之前的 Foo,它希望去创建一个 Deployment,replicas 为 1,这就需要我们创建一个控制器用于创建对应的 Deployment 才能真正实现 CRD 的功能。
控制器工作流程概览
这里以 kube-controller-manager 为例。
如上图所示,左侧是一个 Informer,它的机制就是通过去 watch kube-apiserver,而 kube-apiserver 会去监督所有 etcd 中资源的创建、更新与删除。Informer 主要有两个方法:一个是 ListFunc;一个是 WatchFunc。 ListFunc 就是像 "kuberctl get pods" 这类操作,把当前所有的资源都列出来; WatchFunc 会和 apiserver 建立一个长链接,一旦有一个新的对象提交上去之后,apiserver 就会反向推送回来,告诉 Informer 有一个新的对象创建或者更新等操作。
Informer 接收到了对象的需求之后,就会调用对应的函数(比如图中的三个函数 AddFunc, UpdateFunc 以及 DeleteFunc),并将其按照 key 值的格式放到一个队列中去,key 值的命名规则就是 "namespace/name",name 就是对应的资源的名字。比如我们刚才所说的在 default 的 namespace 中创建一个 foo 类型的资源,那么它的 key 值就是 "default/example-foo"。Controller 从队列中拿到一个对象之后,就会去做相应的操作。
下图就是控制器的工作流程。
首先,通过 kube-apiserver 来推送事件,比如 Added, Updated, Deleted;然后进入到 Controller 的 ListAndWatch() 循环中;ListAndWatch 中有一个先入先出的队列,在操作的时候就将其 Pop() 出来;然后去找对应的 Handler。Handler 会将其交给对应的函数(比如 Add(), Update(), Delete())。
一个函数一般会有多个 Worker。多个 Worker 的意思是说比如同时有好几个对象进来,那么这个 Controller 可能会同时启动五个、十个这样的 Worker 来并行地执行,每个 Worker 可以处理不同的对象实例。
工作完成之后,即把对应的对象创建出来之后,就把这个 key 丢掉,代表已经处理完成。如果处理过程中有什么问题,就直接报错,打出一个事件来,再把这个 key 重新放回到队列中,下一个 Worker 就可以接收过来继续进行相同的处理。
五、本文总结
本文的主要内容就到此为止了,这里为大家简单总结一下: CRD 是 Custom Resources Definition 的缩写,也就是用户自定义资源,用户可以使用这个功能扩展自己的Kubernetes 原生资源信息; CRD 和普通的 Kubernetes 资源一样,都可以受 RBAC 权限控制,并且支持 status 状态字段; CRD-controller 也就是 CRD 控制器,能够实现用户自行编写,并且解析 CRD 并把它变成用户期望的状态。


查看更多:https://yqh.aliyun.com/detail/6310?utm_content=g_1000104985
上云就看云栖号:更多云资讯,上云案例,最佳实践,产品入门,访问:https://yqh.aliyun.com/
多媒体
2020-02-25 16:00:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
钉钉作为国内领先的企业IM工具,在中国有超过亿级别的用户。随着新型冠状病毒肺炎疫情的爆发,大量的企业员工选择了soho模式,企业办公协同工具的需求瞬间爆发。
钉钉作为中国企业办公IM的首选应用,不仅具有项目群、视频通话、视频会议、日报、打卡、远程投屏等基础能力,还具备深度、灵活订制各种 OA (Office Automation,办公室自动化) 应用的能力,极大方便了人与人之间的链接,可以帮助企业员工快速、高效地沟通和协作。
也因此,钉钉迅速冲上了AppStore下载免费榜单的第一位。瞬间爆发的在线办公需求,导致钉钉的访问流量激增。每天早晨的打卡风暴,迅速增长的聊天信息,一次次的流量洪峰冲击着钉钉的业务后台。但是借助于阿里云提供的弹性基础设施,钉钉平稳的渡过了每一次的流量洪峰。
在亿级别的用户体量下,钉钉的消息系统,除了要保证消息及时正确地传递,还要保证已读/未读等特有功能。而且不同于市场上常见的用户级IM工具,企业IM需要实现聊天记录的永久保存,并且提供多端漫游功能。
在用户量持续爆炸性增长的前提下,聊天记录永久保存给钉钉业务带来极大的成本压力。同时在数据爆炸的前提下保证聊天记录的读写性能不降低也是一个极大的挑战。
面对这些挑战,钉钉业务选用了X-Engine作为全量钉钉消息的最终存储引擎,实现了性能和成本的平衡。而且在此次企业协同办公需求爆发期间,钉钉业务的数据库系统顶住了所有的流量洪峰。
为什么是 X-Engine
在X-Engine诞生之前,钉钉采用的是InnoDB引擎。在用户爆发增长,面临存储的压力之后,钉钉考虑了多种候选方案,例如Hbase等nosql服务。但企业IM对数据一致性如事务等功能有比较苛刻的需求,同时业务类型的多样化,也对诸如二级索引等数据库的功能有比较强的依赖。
钉钉消息业务采用X-Engine之后,相同数据所需的磁盘空间比InnoDB引擎减少了62%,同时继续保留了对事务以及二级索引等数据库特性的支持。业务代码不做任何修改,即可以迁移到X-Engine集群上。
钉钉的聊天消息有着非常典型的时效性,即最近发送的消息会被经常访问,而历史的消息则很少被读取。X-Engine天然的冷热分离能力,确保了我们对最新的消息有着最高的处理能力,而对历史的消息有着最高的压缩比,这样的设计兼顾了性能和存储成本。
我们在著名的Link-Bench和阿里巴巴内部交易业务两个数据集上测试了X-Engine的存储空间效率。在测试中,对比开压缩的InnoDB引擎,X-Engine有着2倍空间优势,而对比未开压缩的InnoDB,X-Engine则有着3~5倍的优势。
X-Engine架构
在阐述为什么X-Engine可以节约这么多存储成本之前,我们先看看它的整体架构。
X-Engine采用了LSM-tree典型架构。在LSM-tree的数据组织下,写入的数据首先进入一个内存表中,对该部分数据的读写都是在内存中进行,因此访问最新的数据有着最高的效率。
在内存表的大小达到一定阈值之后,将会冻结进行转储,并持久化到磁盘上。X-Engine在磁盘上的数据也是分层的,随着时间的推移,compaction过程会根据数据的访问频度,将最冷的数据转移到LSM-tree的最底层并进行压缩。因此访问最少最冷的数据有着最长的访问路径,而温度稍高的数据访问路径更短。
数据分层的架构,让我们可以对不同的数据集采用不同的处理模型。例如对于新写入的数据,使用高度并发的事务流水线技术,这项技术可以提高极高的写入吞吐。而对于占整体数据量95%以上的底层数据,使用紧凑编码并进行压缩,以降低空间。而对于底层数据中散布的部分热点数据,则充分使用BlockCache和RowCache来加速读操作。
X-Engine如何有效降低成本
目前广泛使用的InnoDB引擎也是可以通过开启压缩来降低存储空间的,那为什么在钉钉IM的消息存储中,X-Engine有着更好的表现呢?因为X-Engine有以下几个独门秘籍:
🔸紧凑的数据页格式
X-Engine的记录更新使用的是copy-on-write技术,避免原地更新数据页,新数据写入到新的数据页中。由于既有数据不可更新,可以紧凑存储只读数据页面,并使用前缀编码等数据压缩技术,提升页面空间的使用效率。已经失效的历史记录版本则由compaction过程负责清理,保证有效记录都紧凑排列。
Innodb引擎采用传统B+tree组织数据,每个page是一个节点。它在内存中会组织为一个内存堆。为保证插入和更新的效率,InnoDB的page大部分时候都不是全满状态。这种结构虽然有较好的内存原地更新性能,但是持久化到磁盘上,空间使用效率非常低下。
即使初始顺序批量灌入数据创建了紧凑的page结构,后续更新带来的分裂、合并依然会在page中引入缝隙,降低空间使用率。记录在InnoDB的page中的组织和X-Engine的DataBlock中的组织对比如下图所示,可以看到在InnoDB的page中存在大量空洞。
一个经常碰到的场景是:使用InnoDB引擎导入所有基础数据并开启压缩之后,空间表现非常优秀。但是运行一段时间,空间膨胀会非常厉害。这种特性对于数据库实例在不同机器上的弹性调度是非常不利的,而X-Engine则能保证非常稳定的空间占用。
除了LSM-tree带来的天然压缩优势之外,X-Engine团队也探索了其他的数据压缩技术,例如Extent级别的列存,这个可以进一步提升压缩效率到10倍以上。当然在如何在一个TP型存储引擎中,在提升存储效率的前提下保证读取性能是一个值得进一步探讨的话题。
🔸数据压缩及无效记录清理
由于X-Engine的Data Block无需原地更新,经编码之后的数据页,可以使用通用压缩算法(zlib,zstd,snapy等)压缩。所有X-Engine实例中处在LSM-tree中低层次的数据都会默认压缩。
数据压缩是以计算资源换空间的技术,在Data Block的生成时需要消耗CPU资源进行压缩,被压缩的Data Block被访问到时,则需要消耗资源进行解压。因此选用一个压缩率高及压缩/解压速度快的压缩算法也非常关键。
经过大量对比测试,X-Engine默认选用了ZSTD压缩算法,但同时也保留了对其他算法的支持。
除了使用压缩之外, compaction过程会对无效记录进行删除,只保留有效记录。compaction执行的越频繁,无效记录的占比越低,空间使用效率越高。因此保证合适的compaction频率也是提升空间使用效率的关键。
从某种删除无效记录的角度上看,compaciton和压缩一样,代表着一种以计算资源换空间的理念。为了应对compaction对计算资源的消耗,X-Engine团队研发了FPGA compaction技术,使用异构计算设备来加速compaction过程。
压缩是在compaction过程中同步进行的。我们FPAG 上实现compaction算子的同时,也实现了压缩算子,在一个FPGA硬件流水线内同时完成compaction+压缩操作。
虽然FPGA价格不菲,但是经过评估,X-Engine异构计算加速技术能够提供更优异的成本效益比。更详细的数据可以查看论文匠心之作
压缩及compaction可以减少磁盘空间占用。而FPGA等异构计算设备算力的加持让我们可以不用付出性能的代价。即使在没有FPGA加速卡的机器上,借助合理的调度算法,X-Engine也能以较小的性能代价获得存储空间的节省。
🔸智能冷热分离算法
虽然X-Engine有紧凑数据布局的优势, 有ZSTD这样高效的压缩算法,有FPGA提供的澎湃算力。但如何巧妙的利用好这些能力也是一个挑战。其中最核心的问题是保证访问更频繁的数据存储在LSM-tree架构中更高的层级,缓存在BlockCache/RowCache中,缩短这些数据的访问路径。对于那些很少被访问到的数据,可以下沉到LSM-tree的最底层,并压缩存储。
传统LSM-tree的compaction是基于层数阈值或者一层的容量大小阈值触发的,它并不感知数据自身的冷热。一条经常被读取的数据,可能因为时间的推移被推到了LSM-tree的最底层。读取该数据会涉及到对DataBlock的解压,效率低下。因此冷热分离的精准性同时关乎到性能和成本。
为了解决这个问题,我们分析了大量业务的数据访问特征,发现对于绝大部分业务。数据在写入之后,访问频率大致随着时间推移按照指数衰减,但是也会由于某些原因再次变热并被频繁访问。这样一个复杂的特征,传统LRU的模型是难以描述的。
为此我们在X-Engine中除了应用传统的基于统计的模型,还引入了描述能力更强的AI算法。X-Engine中的冷热分离算法主要完成如下几个任务:
compaciton过程挑选出未来最不可能被访问到的数据页和记录,下推到LSM-tree的更底层。
挑选当前热点数据,在compaction或者转储的过程中回填到内存中(BlockCache和RowCache), 避免cache命中率的抖动影响性能。
更进一步的,AI算法会识别出未来可能被访问到的数据,提前preload到内存中,减少首次访问的cache miss。
准确识别出数据冷热,可以避免无效压缩或解压带来的算力浪费,提升热点数据在内存中的命中概率,并最终提升系统吞吐。而要做到冷热识别的完全精准非常困难,我们目前做到了基于概率统计模型来挑选compaction目标数据以及挑选回填到内存中的数据页。基于于未来请求特征的预热算法还在研究中(很快会在新的论文中和大家见面),此技术方向我们也在和各大高校如浙大,北大的研究机构进行合作,也欢迎感兴趣的同学加入我们。
我们有一个先进的LSM-tree引擎X-Engine做基础,同时也有阿里巴巴丰富的业务场景数据做支撑。相信我们能在AI For DB上探索出一条切实可行的道路。
写在最后
X-Engine具有LSM-tree天然分层架构带来的优势,可以使用紧凑的数据页存储格式并结合压缩来降低存储空间开销。我们创新设计的事务流水线处理技术,FPGA异构计算加速技术则提升了X-Engine的性能上限。结合智能化的冷热数据分离技术,X-Engine同时兼具了成本和性能的优势。
除了服务于钉钉业务之外,X-Engine 在阿里巴巴内部也被大量业务所采用,并在线上经过了三四年的锤炼。目前X-Engine也在阿里云RDS MySQL作为一个可选存储引擎售卖,点击阅读原文了解等多详情。
在未来X-Engine作为PolarDB分布式版本的一个底层存储引擎,将在分布式数据库领域服务更多的客户。

查看更多:https://yqh.aliyun.com/detail/6214?utm_content=g_1000104982
上云就看云栖号:更多云资讯,上云案例,最佳实践,产品入门,访问:https://yqh.aliyun.com/
多媒体
2020-02-25 15:45:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
近日,云解析DNS正式发布DNSSEC(Domain Name System Security Extensions)功能。DNSSEC功能的发布,意味着云解析DNS在保护网站访问安全的方面,又前进了一大步。那到底什么是DNSSEC,这项技术的给我们带来了什么好处呢。接下来我们一一道来。
DNS的工作原理
在讲DNSSEC这个功能之前,让我们先来看看DNS的工作原理。我们每个用户,在进行网页浏览的时候,都会输入域名,来打开相应的网页,比如要打开淘宝,我们在浏览器中输入www.taobao.com,浏览器就会将淘宝的页面呈到我们的眼前。当电脑在访问某一个页面的时候,是需要指定IP地址才能进行访问,那么输入域名后,到页面展示中间,发生了一些什么事情呢?首先会去本地DNS服务器去查询,是否存在www.taobao.com这个域名的解析记录,如果能查到,那么就直接将结果返回给用户了。可是如果没有查到怎么办,就需要本地DNS进行递归的流程,依次去根服务器、.com服务器、taobao.com服务器、www.taobao.com服务器上查询,最终获得www.taobao.com 的IP地址,从而浏览器可以展示出淘宝页面。
DNS并非无懈可击
(1)递归链路有被劫持的风险
通过工作原理中的例子我们可以看出,在进行一次递归查询的时候,需要对链路上每一个权威服务器进行请求,接收到应答后再去下一个权威服务器进行查询。有攻击者利用了其中的漏洞:当本地DNS去请求某一个权威DNS服务器的时候,中间的请求很容易就被攻击者冒充或伪造,返回给本地DNS一个错误的解析结果。而由于没有验证手段,此时本地DNS就会拿到错误的解析结果去进行后续的解析,从而网站被重定向到可能有潜在危险的恶意网站。
(2)Local DNS有被投毒的风险
递归解析器可以从权威服务器中接收到DNS数据,并将其进行缓存,当有后续请求时,可以使用缓存数据进行应答,从而加速解析流程。当缓存数据被攻击者模拟权威DNS响应而被递归解析器接收后,缓存中的数据就会变为攻击者的数据。那么后续再进行解析的时候,就都是具有危险性的解析结果了。比如此地址指向了一个钓鱼网站,用户就会在在不知情的情况下丢失了用户密码,给用户和企业带来损失。
如何来确定解析结果是真正权威的结果呢?DNSSEC技术顺势而生,有效保障了解析结果的正确性。
什么是DNSSEC?
(1)DNSSEC的介绍
DNS安全扩展是Internet工程任务组(IETF)提供的一系列DNS安全认证的机制。它是DNS提供给DNS客户端的DNS数据来源进行认证,保证Local DNS(resolver)和权威之间的数据不被篡改(中间人攻击)。当解析数据被篡改后,开启DNSSEC功能的域名,会对获取到的解析数据上的签名进行验签,在验签的过程中,如果失败,则说明获取的解析数据是异常的,则不会使用此解析结果,从而保证用户拿到的解析结果一定是真实可信的。
(2)国内DNSSEC的使用情况
DNSSEC的提议在2009年12月1日宣布,目前正在逐渐普及起来。根据亚太网络信息中心(APNIC)提供的数据显示,全球DNSSEC验证目前为 24.12%。如下图白框内所示,在我国目前只有1.03%的域名使用了DNSSEC功能,如今阿里云云解析DNS已支持了DNSSEC功能,可以为广大用户提供更加稳定的域名解析服务。

图1. 我国DNSSEC占比图
(3)DNSSEC的原理介绍
DNSSEC 的记录类型
• RRSIG(Resource Record Signature): RRset的加密签名
• DNSKEY(DNS Public Key): 公钥,包含KSK(Key Signing Key)和ZSK(Zone Signing Key)公钥两种
• DS(Delegation Signer): KSK公钥的摘要
• NSEC/NSEC3(Next Secure): 用来证明否定应答(no name error, no data error, etc.)
• CDNSKEY和CDS: 子zone用来自动更新在父zone中的DS记录
一个域名,多个相同类型的资源记录的集合成为资源记录集(RRset) ,RRset是DNS传输的基本单元。我们先了解下RRset(esource Record Set)的原理。
RRSET介绍
-即相同owner,type,class的若干RR的集合。如图所示:当有一个域名(example.taobao.com.)下有3个A记录,那么这3个A记录都将绑定到单个 A RRset 中。

图2. RRset介绍
知道了rrset之后,接下来我们来看下签名的工作原理。
ZSK(Zone Signing Key)介绍 ZSK私钥签名RRset生成RRSIG,公钥以DNSKEY RRset的形式发布 仅权威服务器RRset签名生成RRSIG RRSIG的TTL和RRSET的TTL相同 DNS解析时,查询RRset时,会同时获取RRSIG,以及DNSKEY,利用ZSK的公钥验证签名 如果权威DNS是可信的,那么验证过程到这里就可以结束了。可是如果ZSK是伪造的,那该如何处理呢?这就需要有一种方法来对ZSK进行验证

图3. A记录签名流程


图4. A记录签名的验签流程
KSK(Key Signing Key)介绍 KSK用于验证ZSK KSK私钥签名DNSKEY RRset(包含ZSK和KSK的公钥)生成DNSKEY RRSIG 本zone的信任可以建立,但KSK自己签名验证自己,不可信,所以需要建立信任链打通本zone和父zone之间的信任

图5. DNSKEY的签名流程
DS(Delegation Signer)介绍 子zone KSK公钥的哈希,提交到父zone上,子zone提交DS记录后,则意味着子zone的DNSSEC已经准备就绪 递归DNS在迭代查询过程中,权威DNS在返回NS记录的同时会返回DS记录 递归查到KSK公钥后进行哈希,和父zone里的DS记录进行比较,如果能匹配成功,则证明KSK没有被篡改

图6. DS记录的工作原理
解决众多DNSSEC技术难题,只为保护你的域名安全
DNSSEC目前是一个更加安全可靠的DNS解决方案,可以保证用户的解析不被劫持和投毒所影响,使得解析更准确。
这种安全可靠的背后,又有哪些技术挑战需要去克服呢?
(1) 计算量 & 存储量
DNSSEC的拥有两种签名模式:a.在线签名; b.离线签名
在线签名,可以实时的返回最新的签名结果,不需要将签名结果存储在权威DNS上,节省了存储空间,可因为其需要进行实时计算,对CPU的性能带来了很大的考验。
离线签名,可以返回稳定的签名结果,签名结果存储在权威DNS上,节约了计算消耗的时间以及CPU的使用,但对磁盘空间有不小的负担。
云解析DNS在技术上,利用本身的高容量,高性能的优势,解决了DNSSEC功能所带来的计算和存储的压力,保证了用户域名的解析稳定
(2) 签名算法的选择
DNSSEC的签名有多种算法可以选择:
密钥类型编码 密钥类型
1 RSA/MD5
2 Diffie-Hellman
3 DSA/SHA-1
4 Elliptic Curve
5 RSA/SHA-1
6 DSA-NSEC3-SHA1
7 RSASHA1-NSEC3-SHA1
8 RSA/SHA-256
10 RSA/SHA-512
13 ECDSA Curve P-256 with SHA-256
14 ECDSA Curve P-384 with SHA-384
252 Indirect
253 Private DNS
254 Private OID
ECDSA与类似的RSA签名相比,生成ECDSA签名的计算成本要低10倍,对于云解析DNS现在每天有海量的DNS解析请求,我们解决了高性能的签名计算难题,使得DNSSEC在技术上拥有了可行性。
使用ECDSA实现128位签名需要一个256位密钥,而类似的RSA密钥则需要3072位。
综合多种因素,云解析DNS采用的是算法 13: ECDSA Curve P-256 with SHA-256,这种算法签名得到的结果安全性高,并且字符长度适中,在兼顾了安全性的同时,也降低了传输时数据的大小。
(3) 密钥安全性的保障
DNSSEC拥有两种密钥,一种是ZSK,一种是KSK。
其中ZSK是用于对域名的解析记录(例如A记录、CNAME记录等)进行签名使用的。
而KSK是用于对域名的DNSKEY记录进行签名的。
众所周知的Root KSK Ceremonies,就是ICANN为了保证根密钥的安全性,定期轮转ZSK的仪式。
在密钥安全性方面,云解析DNS下了非常大的功夫。为了保证这两种密钥的安全性,云解析使用了全托管的密码机,将密钥托管在密码机中,密钥运算也是在密码机内部进行,从而保证任何人无法看到密钥明文。同时云解析DNS也会定期对密钥进行轮转,通过减少每个密钥加密的数据量,再次提高了密钥的安全性。
DNSSEC 通过使用公钥加密来为授权区域数据进行数字签名,给用户的域名带来稳定的保障,避免解析记录被人篡改和投毒。阿里云云解析DNS已支持DNSSEC,全力为您的域名保驾护航。


查看更多:https://yq.aliyun.com/articles/745133?utm_content=g_1000104632
上云就看云栖号:更多云资讯,上云案例,最佳实践,产品入门,访问:https://yqh.aliyun.com/
多媒体
2020-02-20 16:58:00
「深度学习福利」大神带你进阶工程师,立即查看>>> 最近将族谱数字化,需要用到OCR软件。选择了Tesseract,但是官网的指南不是很亲切,所以记录了一些要点和脚本。
要点 族谱通常是竖向编排的,所以psm(页面分割模式)选项要用5。 合并多图,要先将图单独转换成tif格式,再进行合并。 LSTM引擎生成的盒子都是一条条的,和Legacy引擎框住单个字符的不一样。 LSTM盒子文件每一列文字最后要有一行\t开头的座标以示分隔。 训练时最好使用阈值“ --target_error_rate 0.001 ”。

参考:
https://blog.csdn.net/qq_19313495/article/details/102977915
https://blog.csdn.net/Hu_helloworld/article/details/100923215
脚本 #!/bin/sh export TESSDATA_PREFIX=/usr/share/tesseract/tessdata f_img=$1 nm=${f_img%.*} convert $f_img -density 300 $f_img convert $f_img $nm.tif #生成box tesseract $nm.tif $nm -l chi_sim_vert --psm 5 lstmbox #处理box ##把字符串拆成单独一行,每11行用?分隔 cat uni_string.txt | sed -e 's/\(.\)/\1\n/g' | sed -e '1~11i\?' > uni_char_row.txt #生成lstmf tesseract uni_char.tif uni_char -l chi_sim_vert --psm 5 lstm.train find $(pwd) -name "*.lstmf" > training_files.txt #提取lstm combine_tessdata -e \ /home/ydx/Project-yushizupu/tesseract-train/71-model/yu.traineddata \ /home/ydx/Project-yushizupu/tesseract-train/71-model/yu.lstm #训练 lstmtraining --model_output="./output/output" --continue_from="/home/ydx/Project-yushizupu/tesseract-train/71-model/yu.lstm" \ --train_listfile="/home/ydx/Project-yushizupu/tesseract-train/54/training_files.txt" \ --traineddata="/home/ydx/Project-yushizupu/tesseract-train/71-model/yu.traineddata" \ --debug_interval -1 --target_error_rate 0.01 #产出模型 lstmtraining --stop_training --continue_from="./output/output_checkpoint" \ --traineddata="/home/ydx/Project-yushizupu/tesseract-train/71-model/yu.traineddata" \ --model_output="./model/yu.traineddata" #移动模型 cp ./model/yu.traineddata /home/ydx/Project-yushizupu/tesseract-train/71-model/yu.traineddata sudo cp ./model/yu.traineddata /usr/share/tesseract/tessdata
模型产出
图像:
识别结果对比:
我的模型 - yu. traineddata 提取码:ifkr 十三丑时三子
宏戌宏艺宏达
二女长字大茶
李门次字黄人竹
城曾门氏终十
一月二十申时
葬大岭头向东
公寿八十一终
一九八七年丁
卯八月廿八葬
大石岭向南
官方best模型 - chi_sim_vert.traineddata 十三于时三子
宏成宏艺宏达
二女长字大茶
李门次字黄人竹
城曾门氏终十
-月二十申时
苦大岭头向东
公寿八十一绿
一九八七年本
师八月革八霓
大石叭向南
多媒体
2020-02-19 23:17:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
导读 :Helm 是 Kubernetes 的一个软件包管理器。两个月前,它发布了第三个主要版本,Helm 3。在这一新版本中,有许多重大变化。本文作者将介绍自己认为最关键的 5 个方面。
移除了 Tiller
Helm 最终移除了其服务器端组件,Tiller。现在,它完全没有代理。Tiller 之前是一个运行在 Kubernetes 上的小型应用程序,它用于监听 Helm 命令并处理设置 Kubernetes 资源的实际工作。
这是 Helm3 中最重大的更改。为什么 Tiller 的移除备受关注呢?首先,Helm 应该是一种在 Kubernetes 配置上的模板机制。那么,为什么需要在服务器上运行某些代理呢?
Tiller 本身也存在一些问题,因为它需要集群管理员的 ClusterRole 才能创建。因此,假设你要在 Google Cloud Platform 中启动的 Kubernetes 集群上运行 Helm 应用程序。首先,你需要启动一个新的 GKE 集群,然后使用 helm init 初始化 Helm,然后…发现它失败了。
这种情况之所以会发生是因为,在默认状态下,你没有给你的 kubectl 上下文分配管理员权限。现在你了解到了这一点,开始搜索为分配管理员权限的 magic 命令。这一系列操作下来,也许你已经开始怀疑 Helm 是否真的是一个不错的选择。
此外,由于 Tiller 使用的访问权限与你在 kubectl 上下文中配置的访问权限不同。因此,你也许可以使用 Helm 创建应用程序,但你可能无法使用 kubectl 创建该程序。这一情况如果没排查出来,看起来感觉像是安全漏洞。
幸运的是,现在 Tiller 已经被完全移除,Helm 现在是一个客户端工具。这一更改会导致以下结果: Helm 使用与 kubectl 上下文相同的访问权限; 你无需再使用 helm init 来初始化 Helm; Release Name 位于命名空间中。
Helm 3 一直保持不变的是:它应该只是一个在 Kubernetes API 上执行操作的工具。如此,如果你可以使用纯粹的 kubectl 命令执行某项操作,那么也可以使用 helm 执行该操作。
分布式仓库以及 Helm Hub
Helm 命令可以从远程仓库安装 Chart。在 Helm 3 之前,它通常使用预定义的中心仓库,但你也能够添加其他仓库。但是从现在开始,Helm 将其仓库模型从集中式迁移到分布式。这意味着两个重要的改变: 预定义的中心仓库被移除; Helm Hub(一个发现分布式 chart 仓库的平台)被添加到 helm search。
为了能够更好地理解这一改变,我给你们一个示例。在 Helm 3 之前,如果你想要安装一个 Hazelcast 集群,你需要执行以下命令: $ helm2 install --name my-release stable/hazelcast
现在,这个命令不起作用了。你需要先添加远程仓库才能进行安装。这是因为这里不再存在一个预定义中心仓库。要安装 Hazelcast 集群,你首先需要添加其仓库然后安装 chart: $ helm3 repo add hazelcast https://hazelcast.github.io/charts/ $ helm3 repo update $ helm3 install my- release hazelcast/hazelcast
好消息是现在 Helm 命令可以直接在 Helm Hub 中寻找 Chart。例如,如果你想知道在哪个仓库中可以找到 Hazelcast,你只需执行以下命令即可: $ helm3 search hub hazelcast
以上命令列出在 Helm Hub 中所有分布式仓库中名称中包含 “hazelcast” 的 Chart。
现在,我来问你一个问题。移除掉中心仓库是进步还是退步?这有两种观点。第一种是 chart 维护者的观点。例如,我们维护 Hazelcast Helm Chart,而 Chart 中的每个更改都需要我们将其传播到中心仓库中。这项额外的工作使得中心仓库中的许多 Helm Chart 没有得到很好地维护。这一情况与我们在 Ubuntu/Debian 包仓库中所经历的很相似。你可以使用默认仓库,但它常常只有旧的软件包版本。
第二种观点来自 Chart 的使用者。对于他们来说,虽然现在安装一个 chart 比之前稍微困难了一些,但另一方面,他们能够从主要的仓库中安装到最新的 chart。
JSON Schema 验证
从 Helm 3 开始,chart 维护者可以为输入值定义 JSON Schema。这一功能的完善十分重要,因为迄今为止你可以在 values.yaml 中放入任何你所需的内容,但是安装的最终结果可能不正确或出现一些难以理解的错误消息。
例如,你在 port 参数中输入字符串而不是数字。那么你会收到以下错误: $ helm2 install --name my-release --set service.port=string-name hazelcast/hazelcast Error : release my- release failed : Service in version "v1" cannot be handled as a Service: v1.Service.Spec: v1.ServiceSpec.Ports: []v1.ServicePort: v1.ServicePort.Port: readUint32: unexpected character : �, error found in # 10 byte of ...| "," port ":" wrong- name |..., bigger context ...|fault "}," spec ":{" ports ":[{" name ":" hzport "," port ":" wrong- name "," protocol ": " TCP "," targetPort ":" hazelca|...
你不得不承认这个问题难以分析和理解。
此外,Helm 3 默认添加了针对 Kubernetes 对象的 OpenAPI 验证,这意味着发送到 Kubernetes API 的请求将会被检查是否正确。这对于 Chart 维护者来说,是一项重大利好。
Helm 测试
Helm 测试是一个小小的优化。尽管微小,但它也许实际上鼓励了维护者来写 Helm 测试以及用户在安装完每个 chart 之后执行 helm test 命令。在 Helm 3 之前,进行测试多少都显得有些奇怪: 此前测试作为 Pod 执行(好像需要一直运行);现在你可以将其定义为 Job; 测试 Pod 不会自动被移除(除非你使用 magic flag –cleanup),所以默认状态下,没有任何技巧,对于既定的版本你不能多次执行 helm test。但幸运的是,现在可以自动删除测试资源(Pod、Job)。
当然旧的测试版本也并非不能使用,只需要使用 Pod 并始终记得执行 helm test –cleanup。但也不得不承认,这一改进有助于提升测试体验。
命令行语法
最后一点是,Helm 命令语法有所改变。从积极的一面来看,我认为所有的改变都是为了让体验更好;从消极的方面看,这一语法不与之前的版本兼容。因此,现在编写有关如何使用 Helm 安装东西的步骤时,需要明确指出所使用的命令是用于 Helm 2 还是用于 Helm 3。
举个例子,从 helm install 开始说起。现在版本名称已经成为必填参数,尽管在 Helm 2 中你可以忽略它,名称也能够自动生成。如果在 Helm3 中要达成相同的效果,你需要添加参数 --generate-name。所以,使用 Helm 2 进行标准的安装应该如下: $ helm2 install --name my-release --set service.port=string- $ helm2 install --name my-release hazelcast/hazelcast
在 Helm 3 中,需要执行以下命令: $ helm3 install my- release hazelcast/hazelcast
还有另一个比较好的改变是,删除 Helm 版本后,无需添加— purge。简单地输入命令 helm uninstall 即可删除所有相关的资源。
还有一些其他改变,如一些命令被重命名(不过使用旧的名称作为别名),有一些命令则被删除(如 helm init)。如果你还想了解更多关于 Helm 命令语法更改的信息,请参考官方文档: https://helm.sh/docs/faq/#cli-command-renames
结 论
Helm 3 的发布,使得这一工具迈向一个新的阶段。作为用户,我十分喜欢 Helm 现在只是一个单纯的客户端工具。作为 Chart 维护者,Helm Hub 以及分布式仓库的方法深得我心。我希望能在未来看到更多更有意思的改变。

查看更多:https://yq.aliyun.com/articles/744482?utm_content=g_1000104316
上云就看云栖号:更多云资讯,上云案例,最佳实践,产品入门,访问:https://yqh.aliyun.com/
多媒体
2020-02-18 14:20:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
Photo by freestocks.org on Unsplash Netflix启动了安卓移动客户端上的AV1格式支持,并在尝试将其扩展到硬件等更多场合中。
文 / Coco Liang,蒋默邱泽

6日5日,Netflix在其科技博客宣布在安卓手机移动客户端启用AV1。AV1是一款高性能、免版税的视频编解码器。Netflix透露,AV1的压缩效率比原来使用的VP9编码提高了20%。开放媒体联盟(AOMedia)内对专业知识和知识产权的广泛行业承诺使AV1成为可能,Netflix是该联盟的创始成员之一。
VP9是Netflix在2016年发布的移动编码的一部分,并在2018年通过基于镜头编码进行了进一步优化。
Netflix表示,希望在所有平台上推动AV1部署,他们发现AV1编码的压缩效率更适合移动网络。对于喜欢离线缓存的用户来说,他们现在可以享受AV1编码带来的便利,比如自定义选择字幕。
Netflix在安卓移动端启动的对AV1的支持,还利用了由VideoLAN,VLC和FFmpeg社区构建的开源dav1d解码器。在对dav1d展开优化后,Netflix也得以播放10bit色深的内容。本着让AV1广泛可用的精神去年五月同Intel一同优化迭代SVT-AV1[1],如今我们正发起一项开源工作努力进一步优化10bit性能,并将这些成果开放给所有人。
随着编码器性能的提高,目前与设备和芯片组合作伙伴合作,Netflix希望将AV1扩展到硬件等更多场合中。
2020年,AV1会有更多硬件和设备显示支持,可以肯定是三星和LG在今年会很快支持AV1硬件解码,终端的Roku、Fire TV甚至保守克制的苹果最新Apple TV和Apple 生态加入值得期待。此外,Netflix、Google、Facebook、Mozilla、Tencent、Vimeo等主要目标会放实现可用的编码速度上。

参考
性能可期——Netflix与Intel优化SVT-AV1
多媒体
2020-02-17 11:36:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
写在前面
在本系列的 上一篇 中,我将从 《Qt之美1:数据指针和私有实现》 中的例子抄过来,说明了Qt中源码中(同时也是QtLcation模块中)大量存在的D-P机制。这种设计模式实现了动态库的二进制兼容,同时也隐藏了私有成员和潜在的实现细节;除此之外,还能基于它实现另一种Qt中大量采用的技术,即 “隐式共享技术”。
以下关于“隐式共享技术”的说明内容,大多出自 《Qt中的C++技术》 这本书的第8章《隐式共享与d-pointer技术》。
一般地,一个类型的多个实例所占内存是相互独立的,如果其中某些数据成员的取值完全相同,那么这些实例可共享这些数据成员,当某实例需要修改其中的数据成员时才为该对象重新分配内存;这种技术被称为“隐式共享技术”,又被形象地描述为“写时复制(copy on write)”。如下图所示,假如对象O1、O2、O3和O4都有部分数据成员取值相同,都共享这部分数据成员所占用的内存A;此后需要修改O4位于共享内存A中的某个数据成员,此时不能直接修改内存A,否则会影响O1、O2和O3的数据成员;只好先复制将内存A复制成B,然后重新分配给O4,在B中修改O4需要修改的数据成员。
由于共享内存A的存在,假如要析构O3时,O1和O2还未析构,则不能简单地释放共享内存A,因为这样做会使O1和O2受到牵连。只有当仅存在一个对象引用共享内存时,析构这个仅存的实例才完全释放共享内存,其他时候仅需要“断开”析构实例与共享内存的引用关系即可。这就需要维护一个共享内存的引用计数器(reference counter),每当析构了一个引用该块共享内存的对象时,就减少一个引用计数,直到引用计数为1(即仅存一个引用该内存块的对象)时,下一次再进行减少引用计数的操作(即析构引用该内存的对象)才会完全释放这块内存。
刚才我们讲了 同一类型不同实例 之间实现隐式共享的基本原理,但这并不适用于所有应用场景。其实,Qt已经在“隐式共享技术”上为我们做了很多基础性和示范性工作,其提供了一个名为QSharedData的共享数据类型,还提供了QSharedDataPointer等共享数据类的指针,利用 QSharedData 和QSharedDataPointer等类型就可以实现隐式共享的 进阶操作 ,比如 同一基类不同子类之间的隐式共享 。
了解QGeoShape为基类的地理数据类型
这里以QtPositioning中QGeoShape类型体系的源码作为示例,QGeoShape是所有地理形状类型的基类,其继承者们包括:QGeoRectangle、QGeoCircle、QGeoPath和QGeoPolygon。QGeoShape类似于 Simple Feature标准 中的Geometry类型,但QGeoShape坐标点都是QGeoCoordinate类型,只能表示经纬高坐标。
QGeoShape的源码如下: class QGeoRectangle; class QGeoShapePrivate; class Q_POSITIONING_EXPORT QGeoShape { Q_GADGET Q_PROPERTY(ShapeType type READ type) Q_PROPERTY(bool isValid READ isValid) Q_PROPERTY(bool isEmpty READ isEmpty) Q_ENUMS(ShapeType) public: QGeoShape(); QGeoShape(const QGeoShape &other); ~QGeoShape(); enum ShapeType { UnknownType, RectangleType, CircleType, PathType, PolygonType }; ShapeType type() const; bool isValid() const; bool isEmpty() const; Q_INVOKABLE bool contains(const QGeoCoordinate &coordinate) const; Q_INVOKABLE QGeoRectangle boundingGeoRectangle() const; Q_INVOKABLE QGeoCoordinate center() const; Q_INVOKABLE void extendShape(const QGeoCoordinate &coordinate); bool operator==(const QGeoShape &other) const; bool operator!=(const QGeoShape &other) const; QGeoShape &operator=(const QGeoShape &other); Q_INVOKABLE QString toString() const; protected: QGeoShape(QGeoShapePrivate *d); QSharedDataPointer d_ptr; private: inline QGeoShapePrivate *d_func(); inline const QGeoShapePrivate *d_func() const; };
其给出了私有共享数据类型QGeoShapePrivate的前向声明: class QGeoShapePrivate : public QSharedData { public: explicit QGeoShapePrivate(QGeoShape::ShapeType type); virtual ~QGeoShapePrivate(); virtual bool isValid() const = 0; virtual bool isEmpty() const = 0; virtual bool contains(const QGeoCoordinate &coordinate) const = 0; virtual QGeoCoordinate center() const = 0; virtual QGeoRectangle boundingGeoRectangle() const = 0; virtual void extendShape(const QGeoCoordinate &coordinate) = 0; virtual QGeoShapePrivate *clone() const = 0; virtual bool operator==(const QGeoShapePrivate &other) const; QGeoShape::ShapeType type; };
本来QGeoShapePrivate应该是QGeoShape的具体实现,但是由于QGeoShape被定义为抽象基类,QGeoShapePrivate中的接口都被定义为纯虚函数,实际上QGeoShape接口都是在其子类对应的 私有数据类型 中具体实现的,比如QGeoRectanglePrivate(继承自QGeoShapePrivate): class QGeoRectanglePrivate : public QGeoShapePrivate { public: QGeoRectanglePrivate(); QGeoRectanglePrivate(const QGeoCoordinate &topLeft, const QGeoCoordinate &bottomRight); QGeoRectanglePrivate(const QGeoRectanglePrivate &other); ~QGeoRectanglePrivate(); bool isValid() const override; bool isEmpty() const override; bool contains(const QGeoCoordinate &coordinate) const override; QGeoCoordinate center() const override; QGeoRectangle boundingGeoRectangle() const override; void extendShape(const QGeoCoordinate &coordinate) override; QGeoShapePrivate *clone() const override; bool operator==(const QGeoShapePrivate &other) const override; QGeoCoordinate topLeft; QGeoCoordinate bottomRight; };
QGeoShape及其子类是地图要素的几何数据的主要表达形式。前端的 MapPolyline 、 MapRectangle 、 MapCircle 和 MapPolygon 等地图元素(MapItem),其宿主对象是由后端Qt提供的QDeclarativePolylineMapItem、QDeclarativeRectangleMapItem、QDeclarativeCircleMapItem和QDeclarativePolygonMapItem等类型,它们的地理几何数据(geoShape方法)就分别由QGeoPath、QGeoRectangle、QGeoCircle和QGeoPolygon的实例表示。
下面以QDeclarativeRectangleMapItem源码为例: class Q_LOCATION_PRIVATE_EXPORT QDeclarativeRectangleMapItem: public QDeclarativeGeoMapItemBase { Q_OBJECT Q_PROPERTY(QGeoCoordinate topLeft READ topLeft WRITE setTopLeft NOTIFY topLeftChanged) Q_PROPERTY(QGeoCoordinate bottomRight READ bottomRight WRITE setBottomRight NOTIFY bottomRightChanged) Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged) Q_PROPERTY(QDeclarativeMapLineProperties *border READ border CONSTANT) public: explicit QDeclarativeRectangleMapItem(QQuickItem *parent = 0); ~QDeclarativeRectangleMapItem(); virtual void setMap(QDeclarativeGeoMap *quickMap, QGeoMap *map) override; //from QuickItem virtual QSGNode *updateMapItemPaintNode(QSGNode *, UpdatePaintNodeData *) override; QGeoCoordinate topLeft(); void setTopLeft(const QGeoCoordinate ¢er); QGeoCoordinate bottomRight(); void setBottomRight(const QGeoCoordinate ¢er); QColor color() const; void setColor(const QColor &color); QDeclarativeMapLineProperties *border(); bool contains(const QPointF &point) const override; const QGeoShape &geoShape() const override; QGeoMap::ItemType itemType() const override; Q_SIGNALS: void topLeftChanged(const QGeoCoordinate &topLeft); void bottomRightChanged(const QGeoCoordinate &bottomRight); void colorChanged(const QColor &color); protected: void updatePath(); void geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) override; void updatePolish() override; protected Q_SLOTS: void markSourceDirtyAndUpdate(); virtual void afterViewportChanged(const QGeoMapViewportChangeEvent &event) override; private: QGeoRectangle rectangle_; QDeclarativeMapLineProperties border_; QColor color_; bool dirtyMaterial_; QGeoMapPolygonGeometry geometry_; QGeoMapPolylineGeometry borderGeometry_; bool updatingGeometry_; QList pathMercator_; };
多媒体
2020-02-16 19:50:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
安装gdal 的程序,安装后可以用gdalwarp等程序
apt-get install gdal-bin
安装gdal开发包
apt-get install libgdal-dev
配置环境变量
export CPLUS_INCLUDE_PATH=/usr/include/gdal
export C_INCLUDE_PATH=/usr/include/gdal
通过 gdalinfo --version 查看gdal版本
安装对应的python 接口
pip3 install -i https://pypi.tuna.tsinghua.edu.cn/simple --global-option=build_ext --global-option="-I/usr/include/gdal/" GDAL==2.2.3
多媒体
2020-02-13 21:22:00
「深度学习福利」大神带你进阶工程师,立即查看>>>
Web播放器解决了在手机浏览器和PC浏览器上播放音视频数据的问题,让视音频内容可以不依赖用户安装App,就能进行播放以及在社交平台进行传播。在视频业务大数据平台中,播放数据的统计分析非常重要,所以Web播放器在使用过程中,需要对其内部的数据进行收集并上报至服务端,此时,就需要对发生在其内部的一些播放行为进行事件监听。
那么Web播放器事件监听是怎么实现的呢?
01 监听事件明细表
名称 介绍 play 已经开始播放,调用 play() 方法或者设置了 autuplay 为 true 且生效时触发,这时 paused 属性为 false。
playing 因缓冲而暂停或停止后恢复播放时触发,paused 属性为 false 。通常用这个事件来标记视频真正播放,play 事件只是开始播放,画面并没有开始渲染。
loadstart 开始加载数据时触发。
durationchange 视频的时长数据发生变化时触发。
loadedmetadata 已加载视频的 metadata。
loadeddata 当前帧的数据已加载,但没有足够的数据来播放视频下一帧时,触发该事件。
progress 在获取到媒体数据时触发。
canplay 当播放器能够开始播放视频时触发。
canplaythrough 当播放器预计能够在不停下来进行缓冲的情况下持续播放指定的视频时触发。
error 视频播放出现错误时触发。
pause 暂停时触发。
ratechange 播放速率变更时触发。
seeked 搜寻指定播放位置结束时触发。
seeking 搜寻指定播放位置开始时触发。
timeupdate 当前播放位置有变更,可以理解为 currentTime 有变更。
volumechange 设置音量或者 muted 属性值变更时触发。
waiting 播放停止,下一帧内容不可用时触发。
ended
fullscreenchange
视频播放已结束时触发。此时 currentTime 值等于媒体资源最大值。
全屏状态切换时触发。
02 技术实现
初始化参数
播放器初始化需要传入两个参数,第一个为播放器容器 ID(即video标签上的ID,该ID名称可自定义。例如:),第二个为功能参数对象。
var player = JDplayer('player-video-id', options);
初始化播放器返回监听事件对象的方法
事件监听的技术实现
播放器可以通过初始化返回的对象进行事件监听,示例:
var player = JDplayer('player-video-id', options); // player.on(type, function(){ // 做一些处理 // }); player.on('error', function(error) { // 做一些处理 });
其中 type 为事件类型,具体事件信息详见监听事件明细表。
03 应用场景
Web播放器可广泛应用于视频网站、视频电商、体育/游戏赛事直播、在线教育等场景,而事件监听是Web播放器在实际应用中的重要环节,通过事件监听,可对用户的播放行为、播放异常等数据进行完善的统计分析,这对视频相关业务的规划、运营和维护都有着重要的参考意义。
您也可以点击“ 链接 ”了解更多关于京东云短视频 SDK的相关资讯。
欢迎点击“ 京东云 ”了解更多精彩内容。

多媒体
2020-02-07 13:23:05