Blog

  • Top 10 lỗ hổng bảo mật web phổ biến theo chuẩn OWASP – OWASP TOP 10

    Top 10 lỗ hổng bảo mật web phổ biến theo chuẩn OWASP – OWASP TOP 10

    Dành cho ae lập trình web – Đọc qua mà né

    1. Lỗ hổng Injection (Lỗi chèn mã độc)

    Injection là lỗ hổng xảy ra do sự thiếu sót trong việc lọc các dữ liệu đầu vào không đáng tin cậy. Khi bạn truyền các dữ liệu chưa được lọc tới Database (Ví dụ như lỗ hổng SQL injection), tới trình duyệt (lỗ hổng XSS), tới máy chủ LDAP (lỗ hổng LDAP Injection) hoặc tới bất cứ vị trí nào khác. Vấn đề là kẻ tấn công có thể chèn các đoạn mã độc để gây ra lộ lọt dữ liệu và chiếm quyền kiểm soát trình duyệt của khách hàng.

    Mọi thông tin mà ứng dụng của bạn nhận được đều phải được lọc theo Whitelist. Bởi nếu bạn sử dụng Blacklist việc lọc thông tin sẽ rất dễ bị vượt qua (Bypass). Tính năng Pattern matching sẽ không hoạt động nếu thiết lập Blacklist.

    Cách ngăn chặn lỗ hổng:

    Để chống lại lỗ hổng này chỉ “đơn giản” là vấn đề bạn đã lọc đầu vào đúng cách chưa hay việc bạn cân nhắc  liệu một đầu vào có thể được tin cậy hay không. Về căn bản, tất cả các đầu vào đều phải được lọc và kiểm tra trừ trường hợp đầu vào đó chắc chắn đáng tin cậy.(Tuy nhiên việc cẩn thận kiểm tra tất cả các đầu vào là luôn luôn cần thiết).

    Ví dụ, trong một hệ thống với 1000 đầu vào, lọc thành công 999 đầu vào là không đủ vì điều này vẫn để lại một phần giống như “gót chân Asin”, có thể phá hoại hệ thống của bạn bất cứ lúc nào. Bạn có thể cho rằng đưa kết quả truy vấn SQL vào truy vấn khác là một ý tưởng hay vì cơ sở dữ liệu là đáng tin cậy. Nhưng thật không may vì đầu vào có thể gián tiếp đến từ những kẻ có ý đồ xấu. Đây được gọi là lỗi Second Order SQL Injection.

    Việc lọc dữ liệu khá khó vì thế các bạn nên sử dụng các chức năng lọc có sẵn trong framework của mình. Các tính năng này đã được chứng minh sẽ thực hiện việc kiểm tra một cách kỹ lưỡng. Bạn nên cân nhắc sử dụng các framework vì đây là một trong các cách hiệu quả để bảo vệ máy chủ của bạn.

    2. Broken Authentication

    Đây là nhóm các vấn đề có thể xảy ra trong quá trình xác thực. Có một lời khuyên là không nên tự phát triển các giải pháp mã hóa vì rất khó có thể làm được chính xác.

    Có rất nhiều rủi ro có thể gặp phải trong quá trình xác thực:

    • URL có thể chứa Session ID và rò rỉ nó trong Referer Header của người dùng khác.
    • Mật khẩu không được mã hóa hoặc dễ giải mã trong khi lưu trữ.
    • Lỗ hổng Session Fixation.
    • Tấn công Session Hijacking có thể xảy ra khi thời gian hét hạn của session không được triển khai đúng hoặc sử dụng HTTP (không bảo mật SSL)…

    Cách ngăn chặn lỗ hổng:

    Cách đơn giản nhất để tránh lỗ hổng bảo mật web này là sử dụng một framework. Trong trường hợp bạn muốn tự tạo ra bộ xác thực hoặc mã hóa cho riêng mình, hãy nghĩ đến những rủi ro mà bạn sẽ gặp phải và tự cân nhắc kĩ trước khi thực hiện.

    3. Lỗ hổng XSS (Cross Site Scripting)

    Sơ đồ quá trình tấn công XSS

    Lỗ hổng XSS (Cross-scite Scripting) là một lỗ hổng rất phổ biến. Kẻ tấn công chèn các đoạn mã JavaScript vào ứng dụng web. Khi đầu vào này không được lọc, chúng sẽ được thực thi mã độc trên trình duyệt của người dùng. Kẻ tấn công có thể lấy được cookie của người dùng trên hệ thông hoặc lừa người dùng đến các trang web độc hại.

    Cách ngăn chặn lỗ hổng:
    Có một cách bảo mật web đơn giản đó là không trả lại thẻ HTML cho người dùng. Điều này còn giúp chống lại HTML Injection – Một cuộc tấn công tương tự mà hacker tấn công vào nội dung HTML – không gây ảnh hưởng nghiêm trọng nhưng khá rắc rối cho người dùng. Thông thường cách giải quyết đơn giản chỉ là Encode (chuyển đổi vê dạng dữ liệu khác) tất cả các thẻ HTML. Ví dụ thẻ <script> được trả về dưới dạng <script&gt.

    4. Insecure Direct Object References

    Đây là trường hợp điển hình của việc cho rằng đầu vào của người dùng là tin cậy từ đó dẫn đến lỗ hổng bảo mật. Lỗ hổng này xảy ra khi chương trình cho phép người dùng truy cập các tài nguyên (dữ liệu, file, database). Nếu không thực hiện quá trình kiểm soát quyền hạn (hoặc quá trình này không hoàn chỉnh) kẻ tấn công có thể truy cập một cách bất hợp pháp vào các dữ liệu nhạy cảm, quan trọng trên máy chủ.

    Chúng ta có thể xem xét ví dụ sau:

    Một đoạn mã có module download.php và cho phép người dùng tải tệp xuống sử dụng tham số CGI. Ví dụ download.php?file=something.txt. Do sai sót của nhà phát triển, việc kiểm tra quyền hạn đã bị bỏ qua. Kẻ tấn công có thể sử dụng lỗ hổng này để tải về bất kì tệp nào trên hệ thống mà ứng dụng có quyền truy cập. Chẳng hạn như code ứng dụng, hoặc các dữ liệu khác trên máy chủ.

    Một ví dụ phổ biến khác là chức năng đặt lại mật khẩu dựa vào đầu vào của người dùng để xác định mật khẩu đặt lại. Sau khi nhấp vào URL hợp lệ, kẻ tấn công có thể sửa đổi trường tên người dùng trong URL để “đóng giả” admin.

    Cách ngăn chặn lỗ hổng: Thực hiện phân quyền người dùng đúng cách và nhất quán với sự áp dụng triệt để các Whitelist.

    5. Security Misconfiguration

    Trong thực tế, máy chủ website và các ứng dụng đa số bị cấu hình sai. Có lẽ do một vài sai sót như:

    • Chạy ứng dụng khi chế độ debug được bật.
    • Directory listing
    • Sử dụng phần mềm lỗi thời (WordPress plugin, PhpMyAdmin cũ)
    • Cài đặt các dịch vụ không cần thiết.
    • Không thay đổi default key hoặc mật khẩu
    • Trả về lỗi xử lý thông tin cho kẻ tấn công lợi dụng để tấn công, chẳng hạn như stack traces.

    Cách ngăn chặn lỗ hổng:
    Có một quá trình “xây dựng và triển khai” tốt (tốt nhất là tự động). Cần một quá trình audit các chính xác bảo mật trên máy chủ trước khi triển khai.

    6. Sensitive data exposure (Rò rỉ dữ liệu nhạy cảm)

    Lỗ hổng này thuộc về khía cạnh crypto và tài nguyên. Dữ liệu nhạy cảm phải được mã hóa mọi lúc, bao gồm cả khi gửi đi và khi lưu trữ – không được phép có ngoại lệ. Thông tin thẻ tín dụng và mật khẩu người dùng không bao giờ được gửi đi hoặc được lưu trữ không được mã hóa. Rõ ràng thuật toán mã hóa và hashing không phải là một cách bảo mật yếu. Ngoài ra, các tiêu chuẩn an ninh web đề nghị sử dụng AES (256 bit trở lên) và RSA (2048 bit trở lên).

    Cần phải nói rằng các Session ID và dữ liệu nhạy cảm không nên được truyền trong các URL và cookie nhạy cảm nên có cờ an toàn.

    Cách ngăn chặn lỗ hổng:

    • Sử dụng HTTPS có chứng chỉ phù hợp và PFS (Perfect Forward Secrecy). Không nhận bất cứ thông tin gì trên các kết nối không phải là HTTPS. Có cờ an toàn trên cookie.
    • Bạn cần hạn chế các dữ liệu nhạy cảm có khả năng bị lộ của mình. Nếu bạn không cần những dữ liệu nhạy cảm này, hãy hủy nó. Dữ liệu bạn không có không thể bị đánh cắp.
    • Không bao giờ lưu trữ thông tin thẻ tín dụng, nếu không muốn phải đối phó với việc tuân thủ PCI. Hãy đăng ký một bộ xử lý thanh toán như Stripe hoặc Braintree.
    • Nếu bạn có dữ liệu nhạy cảm mà bạn thực sự cần, lưu trữ mã hóa nó và đảm bảo rằng tất cả các mật khẩu được sử dụng hàm Hash để bảo vệ. Đối với Hash, nên sử dụng bcrypt. Nếu bạn không sử dụng mã hoá bcrypt, hãy tìm hiểu về mã Salt để ngăn ngừa rainbow table attack.

    Không lưu trữ các khóa mã hóa bên cạnh dữ liệu được bảo vệ. Việc này giống như khóa xe mà cắm chìa luôn ở đó. Bảo vệ bản sao lưu của bạn bằng mã hóa và đảm bảo các khóa của bạn là riêng tư.

    7. Missing function level access control (lỗi phân quyền)

    Đây chỉ là sai sót trong vấn đề phân quyền. Nó có nghĩa là khi một hàm được gọi trên máy chủ, quá trình phân quyền không chính xác. Các nhà phát triển dựa vào thực tế là phía máy chủ tạo ra giao diện người dùng và họ nghĩ rằng khách hàng không thể truy cập các chức năng nếu không được cung cấp bởi máy chủ.

    Tuy nhiên, kẻ tấn công luôn có thể yêu cầu các chức năng “ẩn” và sẽ không bị cản trở bởi việc giao diện người dùng không cho phép thực hiện các chức năng này. Hãy tưởng tượng trong giao diện người dùng chỉ có bảng điều khiển/admin và nút nếu người dùng thực sự là quản trị viên. Không có gì ngăn cản kẻ tấn công phát hiện ra những tính năng này và lạm dụng nó nếu không phân quyền.

    Cách ngăn chặn lỗ hổng: Ở phía máy chủ, phải luôn được phân quyền một cách triệt để từ khâu thiết kế. Không có ngoại lệ – mọi lỗ hổng sẽ dẫn đến đủ các vấn đề nghiêm trọng.

    8. Cross Site Request Forgery (CSRF)

    Đây là một ví dụ của cuộc tấn công deputy attack. Trình duyệt bị đánh lừa bởi một số bên thứ ba lạm dụng quyền hạn. 
    Ví dụ: trang web của bên thứ ba gửi yêu cầu đến trang web đích (ví dụ: ngân hàng của bạn) sử dụng trình duyệt của bạn với các dữ liệu như cookie và phiên người dùng. Nếu bạn đang đăng nhập vào một trang trên trang chủ của ngân hàng và trang đó dễ bị tấn công, một tab khác có thể cho phép kẻ tấn công đóng giả người quản trị. Deputy là khi trang web lạm dụng quyền hạn của mình (session cookies) để làm điều gì đó mà kẻ tấn công yêu cầu.

    Chúng ta có thể xem xét ví dụ sau:

    • Kẻ tấn công là Alice chọn mục tiêu là chiếc ví của Todd bằng cách chuyển một phần tiền của Todd cho cô ta. Ngân hàng của Todd đã gặp phải lỗ hổng CSRF. Để gửi tiền, Todd phải truy cập vào URL sau:
    • Sau khi URL này được mở ra, một trang thành công được trình bày cho Todd và việc chuyển đổi đã hoàn tất. Alice cũng biết rằng Todd thường ghé thăm một trang web dưới quyền kiểm soát của cô tại blog.aliceisawesome.com, nơi cô đặt đoạn mã sau đây:
    <img src = "http://example.com/app/transferFunds?amount=1500&destinationAccount=4673243243" width = "0" height = "0" />
    • Khi truy cập trang web của Alice, trình duyệt của Todd nghĩ rằng Alice liên kết đến một hình ảnh và tự động đưa ra yêu cầu HTTP GET để lấy “hình ảnh”, nhưng điều này thực sự hướng dẫn ngân hàng của Todd chuyển $1500 đến Alice.

    Cách ngăn chặn lỗ hổng:

    Lưu trữ một Token bí mật trong một trường form ẩn mà không thể truy cập được từ trang web của bên thứ ba. Tất nhiên bạn phải xác minh trường ẩn này. Một số trang web yêu cầu mật khẩu của bạn cũng như khi sửa đổi các cài đặt nhạy cảm.

    9. Using component with known vulnerabilities

    Đây là vấn đề xảy ra khi sử dụng các bộ thư viện đã tồn tại lỗ hổng. Trước khi tích hợp một mã nguồn mới vào website, hãy thực hiện một số nghiên cứu hoặc kiểm tra bảo mật. Sử dụng mã nguồn mà bạn nhận được từ một người ngẫu nhiên trên GitHub hoặc một số diễn đàn có thể rất thuận tiện. Nhưng hãy sẵn sàng trước nguy cơ đối diện với một lỗ hổng bảo mật web nghiêm trọng.

    Ví dụ: Nhiều trường hợp, trang admin bị lộ không phải vì các lập trình viên sai sót, mà vì phần mềm của bên thứ ba vẫn chưa được cập nhật. Nếu bạn nghĩ rằng họ sẽ không tìm thấy cài đặt phpmyadmin ẩn của bạn, hãy tìm hiểu về dirbuster.

    Cách ngăn chặn lỗ hổng:

    Chú ý cẩn thận khi sử dụng các thành phần của bên thứ 3, không nên là một coder copy-paste. Kiểm tra cẩn thận các đoạn code quan trọng của bạn. Nếu các đoạn code này có lỗ hổng, tin tặc có thể đọc cơ sở dữ liệu, tệp tin cấu hình, mật khẩu… của bạn.

    • Cập nhật mọi thứ: Đảm bảo bạn đang sử dụng phiên bản mới nhất của tất cả mọi thứ và có kế hoạch cập nhật chúng thường xuyên. Ít nhất là đăng ký bản tin về các lỗ hổng bảo mật mới liên quan đến sản phẩm.

    10. Unvalidated redirects and forwards

    Đây lại là vấn đề về lọc đầu vào. Giả sử rằng trang đích có một mô-đun redirect.php lấy URL làm tham số. Thao tác với tham số này có thể tạo ra một URL trên targetite.com chuyển hướng trình duyệt đến địa chỉ malwareinstall.com. Khi người dùng nhìn thấy liên kết, họ sẽ thấy liên kết targetite.com/blahblahblah tin cậy và truy cập vào. Họ ít biết rằng địa chỉ này thực ra chuyển tới trang nhúng phần mềm độc hại (hoặc bất kỳ trang độc hại khác). Ngoài ra, kẻ tấn công có thể chuyển hướng trình duyệt sang targetite.com/deleteprofile?confirm=1.

    Cách ngăn chặn lỗ hổng:

    • Không sử dụng chức năng chuyển hướng.
    • Có một danh sách tĩnh các vị trí hợp lệ để chuyển hướng đến.
    • Có Whitelist tham số người dùng xác định.

    Tổng hợp các cộng cụ quét lỗ hổng website tốt nhất

  • Cách fix lỗi unzip cho những folder tiếng Nhật

    Cách fix lỗi unzip cho những folder tiếng Nhật

    Mô tả lỗi:   

    Bạn nhận được 1 folder tiếng Nhật và sau khi dùng phần mềm giải nén thì tên file sẽ biến thành như này: âlâCâïâvâèâôâ^âAâvâèÄÄì∞èJö¡_î⌐É╧éαéΦîƒôóê╦ùèÅæ

    Nguyên nhân:

    Đó là do win của bạn chưa hỗ trợ định dạng cho tiếng Nhật.

    Cách fix:

    Mình đang dùng Win10 nên mình sẽ hướng dẫn các bạn fix trên win10 nhé. Các version khác các bạn cũng tìm cách fix tương tự.

    Bước 1: Vào Control Panel

    Bước 2: Chọn Clock and Region => sau đó chọn Region

    Bước 3: Chọn Adminstrative

    Bước 4: Chọn Change system locale … sau đó change thành (Japanese (Japan))

    Bước 5: Click OK và restart lại PC

    Kết luận

    Hy vọng với tip này sẽ giúp được một cơ số bạn và dự án tránh được bài toán khó chịu này.

  • Tổng quan về Mobile App

    Tổng quan về Mobile App

    Ghi chú:  Bài viết này chỉ là một góc nhìn chủ quan của tác giả về mảng mobile app. vì vậy có gì không đúng mọi người có thể đóng góp ở phần comment nhé!! Thank.

    Mở đầu:
    Ở thời điểm hiện tại việc xây dựng ứng dụng native không phải là lựa chọn duy nhất để tạo lên một một ứng dụng mobile app. Ngày nay chúng ta có thể dựa vào yêu cầu của khách hàng, các chức năng của sản phẩm để lựa chọn được hướng đi phù hợp hơn. Ta có thể dựa trên vào công nghệ web (HTML5, CSS3 và JavaScript) đang phát triển mạnh mẽ trên mobile. Hoặc tận hưởng những lợi ích của các công cụ phát triển đa nền tảng như React Native hoặc Flutter. Dưới đây, bạn sẽ tìm thấy chìa khóa để giải quyết vấn đề khó khăn này khi chọn phương pháp phát triển ứng dụng di động.

    Native App

    Native app hay còn được gọi là ứng dụng gốc. Vốn dĩ nó có cái tên này là bởi vì nó được viết bằng chính các ngôn ngữ lập trình gốc thần nhất dành riêng cho từng nền tảng cụ thể. Hai nền tảng di động phổ biến nhất hiện nay là Android và iOS (Windows Phone thì đã bị khai tử vào tháng 10/ 2017 ). Từ đó, các ngôn ngữ lập trình tương ứng được chính các công ty mẹ tạo ra phù hợp với từng nền tảng. Chẳng hạn như Apple đã có Swift, Objecive-C được dành cho lập trình ứng dụng trên nền tảng iOS. Lập trình trên Android thì dùng Java, mặc dù đây không phải ngôn ngữ do Google tạo ra.

    Viết Native App nghĩa là lập trình viên sẽ sử dụng IDE, SDK mà nhà sản xuất cung cấp để lập trình ra một ứng dụng, build ứng dụng đó thành file cài và gửi lên App Store để kiểm duyệt. Người dùng sẽ phải tìm ứng dụng trên App Store, tải về máy và chạy.  

    Với những hệ thống lớn, cần đồng bộ, ta vẫn phải viết phần back-end trên server. Server sẽ đưa ra một số API. Native app lấy dữ liệu về máy, truyền dữ liệu lên server thông qua các API này.

    Ưu điểm

    • Tận dụng được toàn bộ những tính năng của device: Chụp ảnh, nghiêng máy, rung, GPS, notification.
    • Có thể chạy được offline.
    • Performance rất nhanh vì code native sẽ được chạy trực tiếp.
    • UX phù hợp với từng nền tảng
    • Là lựa chọn duy nhất cho các ứng dụng game, xử lý hình ảnh hay video …

    Khuyết điểm

    • Cần cài đặt nặng nề (Android Studio, XCode, Android SDK, …), khó tiếp cận.
    • Với mỗi hệ điều hành, ta phải viết một ứng dụng riêng. Khó đảm bảo sự đồng bộ giữa các ứng dụng (1 button trên Android sẽ khác 1 button trên iOS, pop cũng khác).
    • Cần phải submit app lên App Store, mỗi lần update phải thông báo người dùng.
    • Code mệt và lâu hơn so với Mobile Web dẫn đến một khuyết điểm là chi phí phát triển cao.

    Kĩ năng cần có

    • Ngôn ngữ lập trình: Java / Kotlin cho Android, Objective-C / Swift cho iOS
    • Kiến thức chuyên sâu về ứng dụng: View, Action, Adapter trong Android …
    • Cách xây dựng Web Serivce, Restful API, cách gọi API từ device, …

    __________________________________________________________________________

    Hybrid App

    Hybrid App kết hợp những ưu điểm của Mobile Web và Native App. Ta xây dựng một ứng dụng bằng HTML, CSS, Javascript, chạy trên WebView của mobile. Tuy nhiên, Hybrid App vẫn có thể tận dụng những tính năng của device: chụp hình, GPS, rung, ….

    Hybrid App sẽ được viết dựa trên một cross-platform framework: Cordova, Phonegap, Ionic …. Ta sẽ gọi những chức năng của mobile thông qua API mà framework này cung cấp, dưới dạng Javascript. Bạn chỉ cần viết một lần, những framework này sẽ tự động dịch ứng dụng này ra các file cài đặt cho Android, iOS . Một số ứng dụng không quá nặng về xử lý, cần tận dụng chức năng của device sẽ chọn hướng phát triển này.

    Ưu điểm

    • Chỉ cần biết HTML, CSS, JS .
    • Viết một lần, chạy được trên nhiều hệ điều hành
    • Tận dụng được các chức năng của device.

    Khuyết điểm

    • Không ổn định, khó debug. Framework sẽ dịch code của bạn thành code native, việc sửa lỗi ứng dụng khá khó vì bạn không biết code sẽ được dịch ra như thế nào.
    • Performance chậm.
    • Cần cài đặt nhiều thứ (phải cài đặt SDK này nọ thì mới build ứng dụng được).

    Kiến thức cần biết

    • HTML, CSS, Javscript cơ bản.
    • Cách dùng một số framework CSS, Javascript: jQuery Mobile, Ionic Framework, AngularJS, Bootstrap, …
    • Kiến thức về các cross-platform framework: Cordova, Phonegap
    • Cách xây dựng Web Serivce, Restful API, cách gọi API từ device, … (Hybrid app cũng sẽ kết nối với server thông qua API như Native App).

    __________________________________________________________________________

    Cross-Platform App

    Được sinh ra nhằm mục đích để giải quyết bài toán hiệu năng của Hybrid và bài toán chi phí khi mà phải viết nhiều loại ngôn ngữ native cho từng nền tảng di động. Nhưng chúng ta lại hay nhầm lẫn giữa Hybrid AppCross-Platform App, trên thực tế thì chúng khác hoàn toàn nhau. Có lẽ, đặc điểm chung duy nhất giữa chúng là khả năng chia sẻ source code. Lập trình viên chỉ cần lập trình một lần và biên dịch hoặc phiên dịch ra thành nhiều bản Native App tương ứng với từng nền tảng khác nhau.

    Công cụ quan trọng nhất để thực hiện các dự án ứng dụng đa nền tảng (Cross Platform) chính là Frameworks đa nền tảng. Có rất nhiều Framework đa nền tảng. Mỗi loại sẽ có những điểm mạnh và điểm yếu khác nhau. Tùy vào mục tiêu xây dựng App mà lập trình viên sẽ lựa chọn Framework nào cho phù hợp.

    Nổi tiếng và phổ biến nhất là Framework Xamarin. Ngôn ngữ lập trình chủ đạo trong Xamarin là C#, ngoài ra còn có Objective-C, Swift và Java. Ngoài ra, còn một số cái tên mà khá hot đó là React-Native (thằng này có ông bô là Facebook ), Flutter (thằng này có ông bác là Google)…

    Ưu điểm

    • Tận dụng được những tính năng của device: Chụp ảnh, nghiêng máy, rung, GPS, notification.
    • Hiệu năng tương đối ổn định.
    • Tiết kiệm tiền.
    • Hiệu quả về mặt thời gian khi mà bạn muốn phát triển một ứng dụng nhanh chóng.
    • Trải nghiệm người dùng tốt hơn là hybrid app.

    Nhược điểm

    • Hiệu năng sẽ thấp hơn với app native code.
    • Khó học vẫn đòi hỏi kiến thức native code.
    • Vẫn còn có hạn chế từ framework

    Kĩ năng cần có

    • Kiến thức C# (đối với Xamarin ), JS (đối với React-Native ), Dart(đối với Flutter) Objective-C, Swift và Java cơ bản.
    • Kiến thức về một số framework React-Native, Xamarin …

    __________________________________________________________________________

    Web App

    Hướng Mobile Web thường được áp dụng khi các bạn đã có sẵn một website đang hoạt động. Ta sẽ tạo thêm 1 trang web riêng cho mobile, sử dụng HTML, CSS, một số framework hỗ trợ mobile và responsive (Bootstrap, jQuery Mobile, Materialize). Người dùng sẽ trang web dành cho mobile để dùng ứng dụng.

    Các xử lý khác liên quan đến backend như database sẽ được thực hiện phía trên server. Với một số framework như Angular, VueJS … một trang web có thể giống y hệt một ứng dụng di động thật sự.

    Ưu điểm

    • Chỉ cần có kiến thức về web là viết được
    • Viết một lần, chạy được trên mọi hệ điều hành
    • Người dùng không cần phải cài app, có thể vào thẳng trang web
    • Không cần phải thông qua App Store, tiết kiệm tiền
    • Dễ nâng cấp (Chỉ việc nâng cấp web là xong)

    Nhược điểm

    • Với một số máy đời cũ, Web App sẽ bị bể giao diện, hiển thị sai, hoặc javascript không chạy.
    • Performance chậm
    • Không thể tận dụng được các tính năng của di động: Push notification, chụp hình, nghiêng máy, định vị GPS…

    Kĩ năng cần có

    • Kiến thức HTML, CSS, Javascript cơ bản.
    • Kiến thức về một số framework responsive/mobile như: jQuery Mobile, Bootstrap, …
    • Một số framework javascript để viết Single Page Application: AngularJS, VueJS, …

    Kết Bài

    Sorry các bạn bài viết hơi dài, sau khi nhìn tổng quan về mobile app thì các bạn đã chọn cho mình hướng đi nào chưa? còn mình thì sẽ tiếp tục theo hướng Cross-Platform app.
    Cảm ơn các bạn đã đọc đến đây nhé.

    Tham khảo: https://railsware.com/blog/native-vs-hybrid-vs-cross-platform/

  • Unsubscribing from RxJS Observables with Angular

    Unsubscribing from RxJS Observables with Angular

    Có thể bạn đã biết …

    Problem

    Đối với Angular thì RxJS như là xương sống của ứng dụng vậy. RxJS sử dụng khái niệm Observables và Observers, trong đó thì Observables là data sources và Observer là người dùng data. Mỗi khi Observable trả về values, thì ngay lập tức nó sẽ inform cho Observer và các Observer này sẽ sử lý dữ liệu trả về thông qua các operator. Làm việc với Observables nếu không khéo sẽ có khả năng gây memory leak cho ứng dụng. Đó là khi bạn subscribe 1 Observable mà sau đó không unsubscribe nó.

    Solution

    Ta sẽ phải thủ công unsubscribe tất cả các custom Observables trong khi destroyed các component/directive. Nơi tốt nhất để unsubscribe đó là handle trong function handle event OnDestroy. Tuy nhiên, để có thể tự động unsubscribe các subscriptions có 3 cách sau đây:

    • unsubscribe thông qua subscription object
    • takeUntil operator
    • async pipe

    Unsubscribe

    export class UnsubscribeCardComponent implements OnInit, OnDestroy {
      message: string;
      subscription: Subscription;
      constructor(private upperCaseService: UpperCaseService) {}
     
      ngOnInit() {
        this.subscription = this.upperCaseService.getUpperCaseMessage()
          .subscribe((message: string) => this.message = message);
      }
     
      ngOnDestroy(): void {this.subscription.unsubscribe();}
    }	
    

    Cách này là dùng 1 đối tượng Subscription để hold resource và dispose nó khi gọi hàm unsubscribe.

    TakeUntil

    export class TakeUntilCardComponent implements OnInit, OnDestroy {
      message: string;
      private unsubscribe$ = new Subject();
      constructor(private upperCaseService: UpperCaseService) {}
     
      ngOnInit() {
        this.upperCaseService.getUpperCaseMessage()
          .takeUntil(this.unsubscribe$)
          .subscribe((message: string) => this.message = message);
      }
     
      ngOnDestroy(): void {
        this.unsubscribe$.next();
        this.unsubscribe$.complete();
      }
    }
    

    takeUntil sẽ nhận Observable như là 1 tham số, và nó sẽ hủy subscription khi Observable bắn ra giá trị hoặc bị terminate

    AsyncPipe

    export class AsyncPipeCardComponent implements OnInit {
      messageSubscription: Observable<string>;
      constructor(private upperCaseService: UpperCaseService) {}
     
      ngOnInit() {
        this.messageSubscription = this.upperCaseService.getUpperCaseMessage();
      }
    }
    
    HTML template:
    <p>{{messageSubscription | async}}</p>
    

    async pipe sẽ subscribe tới 1 Observable và trả về giá trị mới nhất khi nhận được từ Observable ném ra. Khi component bị destroyed thì async pipe sẽ tự động unsubscribe.

    Kết luận

    Khi 1 component/directive bị destroyed, tất các các custom Observable cần phải có thao tác unsubscribe. Qua qua trình tìm hiểu, cũng như làm Angular mình nhận thấy là: Async pipe là solution tốt nhất vì mọi thì đều tự động được thực hiện, tuy nhiên không phải hoàn cảnh nào cũng dùng async pipe được. Khi không dùng được async pipe bạn hãy chuyền qua dùng takeUntil operator.

  • Swift Generics (Part 2)

    Swift Generics (Part 2)

    Ở phần 1 của bài viết, bạn đã hiểu được Generic là gì, công dụng và cách sử dụng cơ bản của Generic.
    Ở phần 2 của bài viết, bạn sẽ học được:

    • Extending a generic type.
    • Subclass a generic type.
    • Cách sử dụng generic nâng cao.

    Extending a generic type.

    Giả sử bạn có 1 struct Package như sau:

    struct Package<Item> {
        var items = [Item]()
        mutating func push(item: Item) {
            items.append(item)
        }
        
        mutating func popLast() -> Item {
           return items.removeLast()
        }
    }

    Ở đây, Item là 1 generic. Khi bạn extending 1 generic type, bạn không cần khai báo lại <Item>. Item là kiểu dữ liệu generic đã được khai báo trong toàn bộ struct.

    extension Package {
        var firstItem: Item? {
           return items.isEmpty ? nil : items[0]
        }
    }

    extension with a generic where clause

    Giả sử bạn muốn khai báo func isFirstItem(:) như sau:

    Lỗi này tương tự như ở phần 1, khi không phải mọi kiểu dữ liệu trong Swift đều có thể sử dụng toán tử (==) để so sánh. Vậy ở đây, bạn có thể extend generic với 1 mệnh đề where như sau:

    Vì kiểu dữ liệu truyền vào của biến bPerson, mà Person thì không tuân theo Equatable, nên b không có func isFirstItem(:)

    Note: Việc extend 1 generic type với mệnh đề where cho phép bạn thêm 1 điều kiện mới cho extension, vì vậy func isFirstItem(:) chỉ được thêm vào extension khi Item thuộc kiểu Equatable.

    Generic subclass

    Có 2 cách để kế thừa 1 class cha là kiểu generic như sau:

    • Tạo 1 class con kế thừa những thuộc tính và func của class cha nhưng vẫn giữ cho class con là kiểu generic.
    • Tạo 1 class con nhưng là 1 class cụ thể.

    Ở trong ví dụ trên, dễ thấy:

    • Box là một generic subclass của class Package vì bạn muốn chiếc hộp này đựng được bất kì thứ gì, vì vậy Box vẫn là kiểu generic nên khi khởi tạo bạn phải truyền vào một kiểu dữ liệu cụ thể.
    • RosePackage là một subclass cụ thể của class Package, gói RosePackage này sẽ dùng chỉ để đựng hoa hồng.

    Sử dụng generic nâng cao

    Mọi table view cơ bản đều có các func sau:

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
            
    }
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        
    }
    ...

    Trong trường hợp bạn muốn tạo các tableview khác nhau, thì sẽ dẫn đến việc duplicate các đoạn code này.
    -> Sử dụng generic để giảm thiểu duplicate code.
    Example1:

    RoseTableViewController là 1 subclass cụ thể của 1 generic class.

    Ở đây, bạn sử dụng generic để giảm duplicate code như sau:

    • Tạo 1 generic class tên BaseTableViewController chứa các thuộc tính, func cơ bản, và khai báo 1 placeholder T kiểu UITableViewCell để dễ dàng truyền các kiểu cell khác nhau vào.
    • Tạo 1 class RoseCell kiểu UITableViewCell.
    • Tạo class RoseTableViewController extends BaseTableViewController, class này dùng cell kiểu RoseCell nên chúng ta truyền kiểu RoseCell vào.
    • override lại các thuộc tính -> func numberOfRows tự thay đổi theo.

    Example2 : Trong trường hợp bạn muốn mỗi cell có item kiểu dữ liệu bất kì, bạn có thể khai báo 1 class BaseCell kiểu generic, rồi khởi tạo các cell mới extends BaseCell.

    Note: U ở phần khai báo BaseTableViewController và U ở phần khai báo BaseCell là khác nhau, ở đây mình đặt đều là U cho bạn đọc đỡ confuse.

  • Swift Generics (Part 1)

    Swift Generics (Part 1)

    Generic cho phép bạn viết function và type 1 cách linh hoạt, dễ dàng tái sử dụng, có thể hoạt động với bất kì loại nào tùy theo các yêu cầu mà bạn xác định. Với generic, bạn có thể tránh sự trùng lặp của code mà vẫn thể hiện í định của nó 1 cách rõ ràng.

    Ở phần 1 của bài viết, bạn sẽ học được:

    • Generics là gì ?
    • Tại sao chúng hữu dụng ?
    • Cách sử dụng generic đơn giản.

    Getting Started

    Giả sử bạn được yêu cầu viết 1 phương thức tính tổng 2 số với kiểu dữ liệu bất kì. Thông thường, bạn sẽ viết các func riêng cho các kiểu dữ liệu riêng.

    func addInt(num1: Int, num2: Int) -> Int {
        return num1 + num2
    }
    
    func addDouble(num1: Double, num2: Double) -> Double {
        return num1 + num2
    }

    Dễ dàng nhận thấy, func addInt(::) và func addDouble(::) là 2 func khác nhau nhưng có thân hàm giống nhau. Bạn không chỉ có 2 func, mà code của bạn bị lặp lại.
    -> Generic có thể được sử dụng để giảm 2 hàm này thành 1 và loại bỏ sự trùng lặp code.

    Note

    Trong các func ở trên, kiểu dữ liệu của num1num2 là giống nhau. Nếu kiểu dữ liệu của chúng khác nhau thì không thể cộng 2 số.

    Generic Function

    Generic func có thể hoạt động với bất kì kiểu dữ liệu nào. Dưới đây là code generic của addInt(::) ở trên, tên là addTwoNumbers:

    func addTwoNumbers<T>(num1: T, num2: T) -> T {
       return num1 + num2
    }

    Tưởng tượng bạn có 1 UITextfield, UITextfield đó có 1 trường gọi là placeholder. placeholder sẽ được thay thế bằng text khi bạn nhập text vào UITextfield.
    Tương tự như vậy, func addTwoNumbers(::) sử dụng kiểu dữ liệu T như là 1 placeholder. Kiểu dữ liệu T sẽ được thay thế bằng 1 kiểu dữ liệu xác định do bạn quyết định như Int, Double, … mỗi khi func addTwoNumbers(::) dược gọi. Ta có thể thấy trong func addTwoNumbers(::) ở trên, num1 và num2 có cùng kiểu dữ liệu là T.

    Note:

    • Sự khác biệt giữa generic func và non-generic func là: Ở tên của generic func thì có 1 kiểu dữ liệu placeholder (T) được khai báo trong cặp ngoặc nhọn (<T>). Cặp ngoặc nhọn nói với Swift rằng T là kiểu dữ liệu placeholder.
    • Bạn có thể thay T bằng các các tên khác như A, B, C, Element, … tùy bạn muốn. Việc thay đổi tên chỉ có tác dụng thay đổi tên của kiểu dữ liệu placeholder, chứ không thay đổi gì về tác dụng. Nên tránh đặt tên trùng với các kiểu dữ liệu xác định như Int, String, Double, … để tránh gây nhầm lẫn.
    • Và luôn đặt tên bắt đầu bằng chữ cái in hoa để người đọc hiểu rằng đó là 1 placeholder cho 1 kiểu dữ liệu chứ không phải cho 1 giá trị.

    Tuy nhiên, khi khai báo func addTwoNumbers(::) như trên, bạn sẽ gặp lỗi như sau:

    Lỗi này có nghĩa là, bạn không thể áp dụng toán tử + cho kiểu dữ liệu T vì không phải mọi kiểu dữ liệu trong Swift đều có thể cộng với nhau bằng toán tử (+). Để áp dụng được toán tử (+) cho kiểu dữ liệu T, bạn làm như sau:

    protocol Summable {
        static func +(num1: Self, num2: Self) -> Self
    }
    
    func addTwoNumbers<T: Summable>(num1: T, num2: T) -> T {
        return num1 + num2
    }

    Ở đây, bạn khai báo 1 protocol Summable có chứa toán tử (+) và cho T extends Summable. Khi đó có nghĩa là, kiểu dữ liệu xác định mà bạn muốn đưa vào để thay T thì kiểu dữ liệu đó phải extends Summable, và hiển nhiên là chúng sẽ phải khai báo toán tử (+).

    Sử dụng generic:

    Khi bạn gọi hàm addTwoNumbers, bạn sẽ thay T bằng 1 kiểu dữ liệu xác định. Tuy nhiên, vì T là kiểu Summable, nên kiểu dữ liệu xác định mà bạn muốn truyền vào cũng phải extends Summable và khai báo toán tử + cho kiểu dữ liệu đó.

    Example 1: Gọi hàm addTwoNumbers cho 2 số kiểu Int:

    • Trước hết, phải cho Int extend Summable.
    extension Int: Summable {}

    Note: Vì bản thân các kiểu dữ liệu như Int, Double, String, … đã có chứa sẵn toán tử (+), nên không cần khai báo toán tử (+) cho chúng nữa.

    let number1: Int = 5
    let number2: Int = 10
    let total = addTwoNumbers<Int>(num1: number1, num2: number2) // 15

    Example 2: Khởi tạo và khai báo hàm addTwoNumbers(::) cho 2 số kiểu String :v

    Note: Bạn có thể gọi hàm theo cách: addTwoNumbers<String>(num1: string1, num2: string2), hoặc bỏ <String> đi, vì Swift sẽ dựa vào kiểu dữ liệu bạn truyền vào num1, num2 để tự hiểu kiểu dữ liệu xác định bạn muốn thay cho T là String.

    Ở phần tiếp theo, mình sẽ giới thiệu về:

    • Extending a generic type.
    • Subclass a generic type.
    • Cách sử dụng generic nâng cao.
  • Media Player (Part2) – Add Record function vào IJKPlayer trên Android

    Media Player (Part2) – Add Record function vào IJKPlayer trên Android

    Điểm mạnh của IJKPlayer là low latency, nó có độ trễ khá thấp khi streaming, nhưng giả sử phát sinh tình huống cần record một đoạn video khi đang streaming thì phải làm thế nào? IJKPlayer không support sẵn.

    Sau khi tham khảo 1 số blog của các bạn…..Trung Quốc và build được thành công thì mình share lại thông tin cho ai cần(vâng, ko hiểu tại sao cứ liên quan đến video, camera,Streaming thì tài liệu chỉ có thể tìm thấy bên….Trung Quốc. Không phủ nhận, các bạn ý giỏi thật  )

    Trước hết, các bạn cần build được IJKPlayer cho Android đã nhé, cụ thể có thể xem link bên dưới

    OK, Sau khi các bạn biết cách chuẩn bị môi trường và build thành công, chúng ta cần tiến hành chỉnh sửa một chút trong thư viện IJKplayer(Mình sẽ không đề cập đến vấn đề pháp lý, licence ở đây)

    Chúng ta cần check out source code và build thử nhé

    git clone https://github.com/baka3k/IjkPlayerRecorder.git
    cd IjkPlayerRecorder
    // Khởi tạo project build cho Android
    ./init-android.sh
    //IJKPlayer có sử dụng ffmpeg - nên việc build ffmpeg là bắt buộc
    cd android/contrib
    ./compile-ffmpeg.sh clean
    ./compile-ffmpeg.sh all
    
    //quay lại thư mục IJK và build IJKPlayer
    cd ..
    ./compile-ijk.sh all

    Nếu thành công – tức là bạn đã setup đầy đủ, có thể build IJKPlayer được, sẵn sàng cho việc thêm code để thêm chức năng recorder cho IJKPlayer.

    Tiếp tục nhé

    1.Trong thư mục ijkmedia/ijkplayer bạn tìm đến file ff_ffplay.h và khai báo các hàm sau:

    Refer:

    https://github.com/baka3k/IjkPlayerRecorder/blob/master/ijkmedia/ijkplayer/ff_ffplay.h
    /* record rtsp streaming */
    int ffp_start_recording_l(FFPlayer *ffp, const char *file_name);
    int ffp_record_isfinished_l(FFPlayer *ffp);
    int ffp_stop_recording_l(FFPlayer *ffp);
    void ffp_get_current_frame_l(FFPlayer *ffp, uint8_t *frame_buf);

    2.Trong file ff_ffplay.c chúng ta định nghĩa nội dung thân hàm cho 4 function này

    Refer:

    https://github.com/baka3k/IjkPlayerRecorder/blob/master/ijkmedia/ijkplayer/ff_ffplay.c
    int ffp_start_recording_l(FFPlayer *ffp, const char *file_name)
    {
    assert(ffp);
    VideoState *is = ffp->is;
    
    ffp->m_ofmt_ctx = NULL;
    ffp->m_ofmt = NULL;
    ffp->is_record = 0;
    ffp->record_error = 0;
    
    if (!file_name || !strlen(file_name)) {
    av_log(ffp, AV_LOG_ERROR, "filename is invalid");
    goto end;
    }
    
    if (!is || !is->ic || is->paused || is->abort_request) {
    av_log(ffp, AV_LOG_ERROR, "is,is->ic,is->paused is invalid");
    goto end;
    }
    
    if (ffp->is_record) {
    av_log(ffp, AV_LOG_ERROR, "recording has started");
    goto end;
    }
    
    avformat_alloc_output_context2(&ffp->m_ofmt_ctx, NULL, NULL, file_name);
    if (!ffp->m_ofmt_ctx) {
    av_log(ffp, AV_LOG_ERROR, "Could not create output context filename is %s\n", file_name);
    goto end;
    }
    ffp->m_ofmt = ffp->m_ofmt_ctx->oformat;
    
    for (int i = 0; i < is->ic->nb_streams; i++) {
    AVStream *in_stream = is->ic->streams[i];
    AVStream *out_stream = avformat_new_stream(ffp->m_ofmt_ctx, in_stream->codec->codec);
    if (!out_stream) {
    av_log(ffp, AV_LOG_ERROR, "Failed allocating output stream\n");
    goto end;
    }
    
    av_log(ffp, AV_LOG_DEBUG, "in_stream->codec;%p\n", in_stream->codec);
    if (avcodec_copy_context(out_stream->codec, in_stream->codec) < 0) {
    av_log(ffp, AV_LOG_ERROR, "Failed to copy context from input to output stream codec context\n");
    goto end;
    }
    
    out_stream->codec->codec_tag = 0;
    if (ffp->m_ofmt_ctx->oformat->flags & AVFMT_GLOBALHEADER) {
    out_stream->codec->flags |= CODEC_FLAG_GLOBAL_HEADER;
    }
    }
    
    av_dump_format(ffp->m_ofmt_ctx, 0, file_name, 1);
    
    if (!(ffp->m_ofmt->flags & AVFMT_NOFILE)) {
    if (avio_open(&ffp->m_ofmt_ctx->pb, file_name, AVIO_FLAG_WRITE) < 0) {
    av_log(ffp, AV_LOG_ERROR, "Could not open output file '%s'", file_name);
    goto end;
    }
    }
    
    if (avformat_write_header(ffp->m_ofmt_ctx, NULL) < 0) {
    av_log(ffp, AV_LOG_ERROR, "Error occurred when opening output file\n");
    goto end;
    }
    
    ffp->is_record = 1;
    ffp->record_error = 0;
    pthread_mutex_init(&ffp->record_mutex, NULL);
    
    return 0;
    end:
    ffp->record_error = 1;
    return -1;
    }
    
    int ffp_record_isfinished_l(FFPlayer *ffp)
    {
    return 0;
    }
    
    int ffp_record_file(FFPlayer *ffp, AVPacket *packet)
    {
    assert(ffp);
    VideoState *is = ffp->is;
    int ret = 0;
    AVStream *in_stream;
    AVStream *out_stream;
    
    if (ffp->is_record) {
    if (packet == NULL) {
    ffp->record_error = 1;
    av_log(ffp, AV_LOG_ERROR, "packet == NULL");
    return -1;
    }
    
    AVPacket *pkt = (AVPacket *)av_malloc(sizeof(AVPacket));
    av_new_packet(pkt, 0);
    if (0 == av_packet_ref(pkt, packet)) {
    pthread_mutex_lock(&ffp->record_mutex);
    if (!ffp->is_first) {
    ffp->is_first = 1;
    pkt->pts = 0;
    pkt->dts = 0;
    } else {
    if (pkt->stream_index == AVMEDIA_TYPE_AUDIO) {
    pkt->pts = llabs(pkt->pts - ffp->start_a_pts);
    pkt->dts = llabs(pkt->dts - ffp->start_a_dts);
    }
    else if (pkt->stream_index == AVMEDIA_TYPE_VIDEO) {
    pkt->pts = pkt->dts = llabs(pkt->dts - ffp->start_v_dts);
    }
    }
    
    in_stream = is->ic->streams[pkt->stream_index];
    out_stream = ffp->m_ofmt_ctx->streams[pkt->stream_index];
    
    pkt->pts = av_rescale_q_rnd(pkt->pts, in_stream->time_base, out_stream->time_base, (AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX));
    pkt->dts = av_rescale_q_rnd(pkt->dts, in_stream->time_base, out_stream->time_base, (AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX));
    pkt->duration = av_rescale_q(pkt->duration, in_stream->time_base, out_stream->time_base);
    pkt->pos = -1;
    
    if ((ret = av_interleaved_write_frame(ffp->m_ofmt_ctx, pkt)) < 0) {
    av_log(ffp, AV_LOG_ERROR, "Error muxing packet\n");
    }
    
    av_packet_unref(pkt);
    pthread_mutex_unlock(&ffp->record_mutex);
    } else {
    av_log(ffp, AV_LOG_ERROR, "av_packet_ref == NULL");
    }
    }
    return ret;
    }
    
    int ffp_stop_recording_l(FFPlayer *ffp){
    assert(ffp);
    if (ffp->is_record) {
    ffp->is_record = 0;
    pthread_mutex_lock(&ffp->record_mutex);
    if (ffp->m_ofmt_ctx != NULL) {
    av_write_trailer(ffp->m_ofmt_ctx);
    if (ffp->m_ofmt_ctx && !(ffp->m_ofmt->flags & AVFMT_NOFILE)) {
    avio_close(ffp->m_ofmt_ctx->pb);
    }
    avformat_free_context(ffp->m_ofmt_ctx);
    ffp->m_ofmt_ctx = NULL;
    ffp->is_first = 0;
    }
    pthread_mutex_unlock(&ffp->record_mutex);
    pthread_mutex_destroy(&ffp->record_mutex);
    av_log(ffp, AV_LOG_DEBUG, "stopRecord ok\n");
    } else {
    av_log(ffp, AV_LOG_ERROR, "don't need stopRecord\n");
    }
    return 0;
    }
    void ffp_get_current_frame_l(FFPlayer *ffp, uint8_t *frame_buf)
    {
    ALOGD("=============>start snapshot\n");
    
    VideoState *is = ffp->is;
    Frame *vp;
    int i = 0, linesize = 0, pixels = 0;
    uint8_t *src;
    
    vp = &is->pictq.queue[is->pictq.rindex];
    int height = vp->bmp->h;
    int width = vp->bmp->w;
    
    ALOGD("=============>%d X %d === %d\n", width, height, vp->bmp->pitches[0]);
    
    // copy data to bitmap in java code
    linesize = vp->bmp->pitches[0];
    src = vp->bmp->pixels[0];
    pixels = width * 4;
    for (i = 0; i < height; i++)
    {
    memcpy(frame_buf + i * pixels, src + i * linesize, pixels);
    }
    ALOGD("=============>end snapshot\n");
    }

    3. Define thêm các cấu trúc dữ liệu cần thiết trong ff_ffplay_def.h

    Refer(line 724->733)

    https://github.com/baka3k/IjkPlayerRecorder/blob/master/ijkmedia/ijkplayer/ff_ffplay_def.h
    AVFormatContext *m_ofmt_ctx;
    AVOutputFormat *m_ofmt;
    pthread_mutex_t record_mutex;
    int is_record;
    int record_error;
    int is_first;
    int64_t start_v_pts;
    int64_t start_v_dts;
    int64_t start_a_pts;
    int64_t start_a_dts;

    4. Define thêm hàm record trong ijkplayer.c(đây chính là class player mà chúng ta sẽ phải viết thêm JNI để câu xuống)

    Define hàm vào header file ijkplayer.h

    Refer

    https://github.com/baka3k/IjkPlayerRecorder/blob/master/ijkmedia/ijkplayer/ijkplayer.h
    int ijkmp_start_recording(IjkMediaPlayer *mp, const char *filePath);
    int ijkmp_stop_recording(IjkMediaPlayer *mp);
    int ijkmp_isRecording(IjkMediaPlayer *mp);
    void ijkmp_get_current_frame(IjkMediaPlayer *mp,uint8_t *frame_buf);

    Viết thân hàm vào ijkplayer.c

    Refer

    https://github.com/baka3k/IjkPlayerRecorder/blob/master/ijkmedia/ijkplayer/ijkplayer.c
    static int ijkmp_start_recording_l(IjkMediaPlayer *mp, const char *filePath)
    {
    av_log(mp->ffplayer,AV_LOG_INFO,"cjz ijkmp_start_recording_l filePath %s",filePath);
    return ffp_start_recording_l(mp->ffplayer, filePath);
    }
    
    int ijkmp_start_recording(IjkMediaPlayer *mp,const char *filePath)
    {
    assert(mp);
    pthread_mutex_lock(&mp->mutex);
    av_log(mp->ffplayer,AV_LOG_WARNING,"cjz ijkmp_start_recording --- ");
    int retval = ijkmp_start_recording_l(mp,filePath);
    printf("ijkmp_start_recording return == %d\n",retval);
    pthread_mutex_unlock(&mp->mutex);
    return retval;
    }
    
    static int ijkmp_stop_recording_l(IjkMediaPlayer *mp)
    {
    return ffp_stop_recording_l(mp->ffplayer);
    }
    
    int ijkmp_stop_recording(IjkMediaPlayer *mp)
    {
    assert(mp);
    pthread_mutex_lock(&mp->mutex);
    av_log(mp->ffplayer,AV_LOG_WARNING,"cjz ijkmp_stop_recording");
    int retval = ijkmp_stop_recording_l(mp);
    pthread_mutex_unlock(&mp->mutex);
    return retval;
    }
    
    static int ijkmp_isRecordFinished_l(IjkMediaPlayer *mp)
    {
    return ffp_record_isfinished_l(mp->ffplayer);
    }
    
    int ijkmp_isRecordFinished(IjkMediaPlayer *mp)
    {
    assert(mp);
    pthread_mutex_lock(&mp->mutex);
    av_log(mp->ffplayer,AV_LOG_WARNING,"cjz ijkmp_isRecordFinished ");
    int retval = ijkmp_isRecordFinished_l(mp);
    pthread_mutex_unlock(&mp->mutex);
    return retval;
    }
    
    int ijkmp_isRecording(IjkMediaPlayer *mp) {
    return mp->ffplayer->is_record;
    }
    void ijkmp_get_current_frame(IjkMediaPlayer *mp,uint8_t *frame_buf){
    ffp_get_current_frame_l(mp->ffplayer,frame_buf);
    }

    5. Cuối cùng, update thêm JNI để có thể call được từ tầng java xuống C nhé

    Refer:

    https://github.com/baka3k/IjkPlayerRecorder/blob/master/ijkmedia/ijkplayer/android/ijkplayer_jni.c

    Các bạn tìm đến đường dẫn IjkPlayerRecorder/ijkmedia/ijkplayer/android/ijkplayer_jni.c

    Và thêm vào

    static jint
    IjkMediaPlayer_startRecord(JNIEnv *env, jobject thiz,jstring file)
    {
    jint retval = 0;
    IjkMediaPlayer *mp = jni_get_media_player(env, thiz);
    JNI_CHECK_GOTO(mp, env, NULL, "mpjni: startRecord: null mp", LABEL_RETURN);
    const char *nativeString = (*env)->GetStringUTFChars(env, file, 0);
    retval = ijkmp_start_recording(mp,nativeString);
    
    LABEL_RETURN:
    ijkmp_dec_ref_p(&mp);
    return retval;
    }
    
    static jint
    IjkMediaPlayer_stopRecord(JNIEnv *env, jobject thiz)
    {
    jint retval = 0;
    IjkMediaPlayer *mp = jni_get_media_player(env, thiz);
    JNI_CHECK_GOTO(mp, env, NULL, "mpjni: stopRecord: null mp", LABEL_RETURN);
    
    retval = ijkmp_stop_recording(mp);
    
    LABEL_RETURN:
    ijkmp_dec_ref_p(&mp);
    return retval;
    }
    
    static jboolean
    IjkMediaPlayer_getCurrentFrame(JNIEnv *env, jobject thiz, jobject bitmap)
    {
    jboolean retval = JNI_TRUE;
    IjkMediaPlayer *mp = jni_get_media_player(env, thiz);
    JNI_CHECK_GOTO(mp, env, NULL, "mpjni: getCurrentFrame: null mp", LABEL_RETURN);
    
    uint8_t *frame_buffer = NULL;
    
    if (0 > AndroidBitmap_lockPixels(env, bitmap, (void **)&frame_buffer)) {
    (*env)->ThrowNew(env, "java/io/IOException", "Unable to lock pixels.");
    return JNI_FALSE;
    }
    
    ijkmp_get_current_frame(mp, frame_buffer);
    
    if (0 > AndroidBitmap_unlockPixels(env, bitmap)) {
    (*env)->ThrowNew(env, "java/io/IOException", "Unable to unlock pixels.");
    return JNI_FALSE;
    }
    
    LABEL_RETURN:
    ijkmp_dec_ref_p(&mp);
    return retval;
    }

    6. Chỉnh sửa các function để JNI map với java

    Trong file ijkplayer_jni.c, tìm đến line 1190 có đoạn define

    static JNINativeMethod g_methods[] = {....

    Add thêm

    { "startRecord", "(Ljava/lang/String;)I", (void *) IjkMediaPlayer_startRecord },
    { "stopRecord", "()I", (void *) IjkMediaPlayer_stopRecord },
    { "getCurrentFrame", "(Landroid/graphics/Bitmap;)Z", (void *) IjkMediaPlayer_getCurrentFrame },

    Refer

    https://github.com/baka3k/IjkPlayerRecorder/blob/master/ijkmedia/ijkplayer/android/ijkplayer_jni.c

    Việc update code đến đây là OK

    Chúng ta build lại IJKPlayer

    cd android/contrib
    ./compile-ffmpeg.sh clean
    ./compile-ffmpeg.sh all
    
    cd ..
    ./compile-ijk.sh all

    Chờ đợi khá lâu đấy, nếu build lần đầu tiên có khả năng mất 1 tiếng :(, sau khi kết thúc build các bạn sẽ tìm thấy các file .so trong thư mục

    - /IjkPlayerRecorder/android/ijkplayer/ijkplayer-armv7a/src/main/libs/armeabi-v7a
    - /IjkPlayerRecorder/android/ijkplayer/ijkplayer-armv5/src/main/libs/armeabi
    - /IjkPlayerRecorder/android/ijkplayer/ijkplayer-arm64/src/main/libs/arm64-v8a

    Copy các file SO này vào project sample

    IjkPlayerRecorder/android/ijkplayer/ijkplayer-example
    

    và thử thôi

  • Git Command Tips for Pros – Chap 1

    Git Command Tips for Pros – Chap 1

    Hôm nay mình sẽ tiếp tục với Git, bài này thực ra định để trong Series: Git from noob to master, tuy nhiên thì thấy cũng không hay ho lắm, nên quyết định tách riêng.

    Những ai cần đọc bài này

    Thực ra thì ai cũng đọc được, nhưng dùng thì không phải ai cũng cần, ví dụ nếu bạn quen thuộc với git graph (Tool Git GUI nào cũng có) và bạn thực sự hiểu thì cũng không cần lắm, tuy nhiên có một số command vẫn cần thiết, vì Tool không bao giờ toàn năng cả.

    Super Strong git help everyday


    git help everyday
    

    Câu lệnh này thực sự strong,

    Nó sẽ chỉ cho bạn biết các câu lệnh bạn cần dùng với các use-case, role khác nhau, ví dụ: Bạn là thằng dev đơn thuần hay là thằng quản lý repository.

    Git log với các tham số

    Thường thì ae sẽ làm như này

    git log
    

    hay ấn nhiều hơn một chút

    git log --oneline --graph
    

    để có cái sự đẹp đẽ như này

    Về cơ bản đều sử dụng git log, git log hỗ trợ một số tham số dòng lệnh, rất mạnh mẽ, đặc biệt là khi được sử dụng kết hợp để filter, đây là mấy cái khá hay ho

    • –author = "Son Hoang" – Chỉ hiển thị các cam kết được thực hiện bởi một tác giả nào đó, tất nhiên là dùng tên hiển thị
    • –name-only – Chỉ hiển thị tên của các tệp đã thay đổi
    • –oneline – Hiển thị dữ liệu cam kết được nén thành một dòng
    • –graph – Hiển thị cây phụ thuộc cho tất cả các xác nhận
    • –reverse – Hiển thị các cam kết theo thứ tự ngược lại (Cam kết cũ nhất trước)
    • — after – Hiển thị tất cả các cam kết đã xảy ra sau một số ngày nhất định
    • — before – Hiển thị tất cả các cam kết đã xảy ra trước một số dữ liệu nhất định Ví dụ: tôi cần tạo report hàng tuần được gửi mỗi thứ Sáu. Vì vậy, mỗi thứ Sáu, tôi sẽ chạy: git log --author="Alex Kras" --after="1 week ago" --oneline, chỉnh sửa một chút là xong.

    Anw: git log có nhiều param khi sử dụng CLI, nó thực sự mạnh và khá tiện hơn so với các GUI Tool, hãy đọc man git-log, một cái nữa --pretty khá là hay ho :).

    À vẫn có phần 2 nữa nhé

  • Media Player (Part1) – RTSP Player Android

    Media Player (Part1) – RTSP Player Android

    Có nhiều cách để play RTSP trên Android, ứng cử viên hàng đầu là VideoView sẵn có trên Android SDK. Tuy nhiên Video View có rất nhiều hạn chế khi chúng ta cần custom lại, ví dụ chỉnh sửa thêm thắt vào protocol,  add các hiệu ứng hình ảnh vào video khi đang play, record, chuyển đổi các track..etc. Khi đó chúng ta phải lựa chọn một giải pháp khác, cụ thể ở đây mình đang nói đến

    1. Exoplayer
    2. IJKPlayer

    Exoplayer (https://github.com/google/ExoPlayer) open source, apache licence, java core, rất dễ dàng để anh em lập trình android/java tiếp xúc nhưng điểm trừ của nó là độ trễ – low latency

    Khi test với 1 số server RTSP, ExoPlayer cho ra độ trễ vào khoảng 1.2s đến 2.5s tùy chất lượng video out put. Còn IJKPlayer(https://github.com/bilibili/ijkplayer) thì có thể đạt 0.6s đến 0.8s.  Điểm trừ của IJKPlayer là licence, IJKPlayer sử dụng FFMPEG, một thư viện rất nổi tiếng, nên nếu ko muốn dính dáng đến pháp lý khi bán sản phẩm, bạn nên cẩn thận khi lựa chọn nó cho khách hàng hoặc nhúng vào sản phẩm.

    Để build được IJKPlayer cũng khá đơn giản(nếu bạn chuẩn bị đủ môi trường), hướng dẫn này mình viết chạy trên môi trường MAC OS nhé, Window hoặc Ubuntu cách làm tương tự, tuy nhiên môi trường chuẩn bị sẽ khác đi 1 chút

    Check out source code

    Các bạn có thể lấy source IJK từ nhánh chính ở trên hoặc từ repo bên dưới, repo này mình đã add thêm một vài  chỉnh sửa liên quan đến RTSP và Recorder

    Option 1: https://github.com/baka3k/IjkPlayerRecorder(có recorder)

    Option 2: https://github.com/bilibili/ijkplayer(nguyên bản)

    Cài đặt NDK cho Mac: mình test thực tế thì thấy build trên bản NDK 10, NDK16(khá cũ) thì ok nhất, ko cần sửa lỗi và chỉnh lại config build, tất nhiên các bạn pro hơn có thể thử với version NDK khác

    https://dl.google.com/android/repository/android-ndk-r10e-darwin-x86_64.zip

    Giải nén NDK, chỉnh lại enviroment path về thư mục NDK vừa download

    Với window thì System>Advanced System setting>Enviroment variable, giống khi các bạn add path cho adb hoặc sdk

    Với Mac OS thì sửa file  ~/.bash_profile hoặc ~/.zshrc hoặc ~/.profile – tùy thuộc vào bạn đang dùng command build nào nhé 🙂

    # export ANDROID_SDK=<your sdk path> # export ANDROID_NDK=<your ndk path>

    Buid nào:

    Kiểm tra kết nối internet(khi build cần tải 1 số package về, nên tốt nhất là full mạng ko qua proxy gì cả)

    Install home brew, git, yasm (chạy lần lượt các lệnh sau theo step nhé)

    # install homebrew, git, yasm
    ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
    brew install git
    brew install yasm

     Build Android

    git clone https://github.com/baka3k/IjkPlayerRecorder.git
    cd IjkPlayerRecorder
    ./init-android.sh
    
    cd android/contrib
    ./compile-ffmpeg.sh clean
    ./compile-ffmpeg.sh all
    
    cd ..
    ./compile-ijk.sh all

    Sau khi build xong các bạn có thể tìm thấy file .so trong thư mục

    - /IjkPlayerRecorder/android/ijkplayer/ijkplayer-armv7a/src/main/libs/armeabi-v7a
    - /IjkPlayerRecorder/android/ijkplayer/ijkplayer-armv5/src/main/libs/armeabi
    - /IjkPlayerRecorder/android/ijkplayer/ijkplayer-arm64/src/main/libs/arm64-v8a

    Và có thể copy những file .so này vào thư mục sample sẵn có để chạy thử

    Sample đặt tại đường dẫn

    - IjkPlayerRecorder/android/ijkplayer/ijkplayer-example

    Lúc này nó như là 1 app android thông thường, đã có sẵn JNI để call xuống .so nhé 🙁

    Have fun 🙂

  • Git from noob to master – Chapter 2

    Git from noob to master – Chapter 2

    Trong một vài lần chém gió về Git, thấy mọi người có vẻ chưa chú ý nhiều đến Git và sử dụng nó một cách hiệu quả. Nhân đây xin mạn phép chém gió sơ qua để mọi người hoàn thiện.

    Git command

    Merge và Rebase (cont.)

    Tiếp theo lần trước, mình sẽ bắt đầu với merge

    Tại sao cần "merge"?.

    Đơn giản là vì chúng ta lập trình cùng nhau, nếu bạn quen dùng SVN, sẽ thấy là khi 2 người cùng làm việc (tất nhiên là song song với nhau), sẽ nảy sinh nhu cầu tạo một phiên bản là sự kết hợp giữa 2 phiên bản source code của 2 người

    Để merge được chúng ta cần 2 branch, dưới dây là một ví dụ

    alt text

    Chúng ta có 2 issue 55 và 56 trên hệ thống tracking, một cách nào đó developer có thể giải quyết 2 issues bằng một commit nhưng không phải lúc nào cũng thế và đặc biệt là không phải lúc nào cũng tốt, như là khi 1 trong 2 issues đó chưa được giải quyết triệt để tức là commit đó có vấn đề, rất khó để trace.

    Mọi người nên làm cách clear hơn, 1 hoặc 2 developer sẽ fix nó, ở các nhánh khác nhau.

    git checkout -b issue55
    git checkout -b issue56
    
    # -b for new branch
    

    alt text

    Họ modify source code ở mỗi nhánh và commit

    alt text

    Giờ chúng ta cần đưa phần thay đổi của 2 branch issue55 và issue56 vào master.

    git checkout master # change currrent branch to master
    git merge issue55
    ...
    git merge issue56
    

    alt text

    alt text

    Như vậy mỗi khi merge, chúng ta kết hợp 2 branch (hoặc commit, phần 3 mình sẽ giải thích kỹ hơn tại sao lại là commit) vào một nhánh, hoặc nghĩ đơn giản hơn là đưa những thay đổi ở nhánh này vào nhánh kia bằng một commit mới (chứa toàn bộ thay đổi).

    Ops!!! Git merge conflict

    alt text

    Tại sao conflict, đơn giản thôi, khi ở nhánh bạn merge vào, trong ví dụ issues55 là master bạn cũng có thay đổi. Ở ví dụ này là chúng ta có như sau

    alt text

    Mình có thay đổi ở master và issues1 cùng một dòng, git sẽ không biết chọn sự thay đổi nào. Nhiêu bạn sẽ sợ conflict và không biết làm thế nào và hỏi, câu trả lời là không thế nào cả, người merge sẽ là người quyết định. Có thể là chọn một trong 2, chọn cả 2 hoặc có thể phải làm lại cả đoạn đó.

    alt text

    Nghe nè: đừng bao giờ giải quyết conflict khi bạn không biết người khác vừa thay đổi cái gì, thật đấy.

    Rebase

    Cũng là một cách khác để đưa những thay đổi từ nhánh này sang nhánh khác nhưng khác hơn một chút: Bạn sẽ không tạo thêm commit mới mà đưa toàn bộ commit từ nhánh này sang nhánh kia.

    Quay lại với ví dụ conflict ở bên trên. Khi merge master vào nhánh issues1 chúng ta có

    alt text.

    Rebase thì sao:

    git checkout issues1
    git rebase master
    .... # conflict and resolve
    git add HelloWorldApp.java # add conflict file to tracking again
    git rebase --continue
    

    alt text.

    Như vậy có thể thấy là thay vì tạo commit mới, rebase sẽ đưa toàn bộ commit ở branch issues1 từ lúc branch out đặt lên cuối branch master, như vậy coi như là chúng ta không branch out từ một điểm trong quá khứ mà branch out từ lastest commit của master. À quên, vẫn có conflict và giải quyết nó như bình thường nhé :D.

    Advance Rebase (super strong)

    Thế giờ nếu có một nhánh khác được branch out từ nhánh issues1 mình có rebase lên master được không.

    Câu trả lời là có:

    Giả sử chúng ta có nhánh issues2 được branch out từ masterissues3 được branch out từ issues2.

    alt text.

    git rebase --onto master issues2 issues3
    

    Về cơ bản nó có ý nghĩa như sau:

    Hãy checkout nhánh issues3, và tìm các commit từ commit chung của nhánh issues3issues2, sau đó apply chúng vào nhánh master.

    alt text.

    Chú ý: không được rebase các commit mà bạn đã push lên repository. Nếu không ai cũng ghét và mọi người sẽ sỉ nhục và coi thường.

    Cơ bản là thế này, khi rebase bạn bỏ đi các commit và chuyển chúng sang chỗ khác. Khi bạn sửa các commit bằng rebase, từ một nhánh bạn pull từ repository, và push lên. Mọi người sẽ phải apply lại commit của họ và sẽ xảy ra nhất nhiều conflict khi bạn pull change các commit đó của họ về local.